java 多线程

一、基础知识点:

1、并发与并行

并行,表示两个线程同时做事情。

并发,表示一会做这个事情,一会做另一个事情,存在着调度。单核 CPU 不可能存在并行(微观上)。


2、临界区

 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

3、阻塞与非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。

此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。阻塞是指线程在操作系统层面被挂起。阻塞一般性能不好,需大约8万个时钟周期来做调度。

非阻塞则允许多个线程同时进入临界区。

4、死锁

死锁是进程死锁的简称,是指多个进程循环等待他方占有的资源而无限的僵持下去的局面。

5、活锁

假设有两个线程1、2,它们都需要资源 A/B,假设1号线程占有了 A 资源,2号线程占有了 B 资源;由于两个线程都需要同时拥有这两个资源才可以工作,为了避免死锁,1号线程释放了 A 资源占有锁,2号线程释放了 B 资源占有锁;此时 AB 空闲,两个线程又同时抢锁,再次出现上述情况,此时发生了活锁。
简单类比,电梯遇到人,一个进的一个出的,对面占路,两个人同时往一个方向让路,来回重复,还是堵着路。
如果线上应用遇到了活锁问题,恭喜你中奖了,这类问题比较难排查。

6、饥饿

饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。


7、线程定义

在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体线程本身是不会独立存在的进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程至少有一个线程,进程中的多个线程是共享进程的资源的。

操作系统在分配资源时候是把资源分配给进程的,但是 CPU 资源就比较特殊,它是分派到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位

Java 中当我们启动 main 函数时候其实就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程。


8、程序计数器、栈、堆

如图一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域。
其中程序计数器是一块内存区域用来记录线程当前要执行的指令地址,那么程序计数器为何要设计为线程私有的呢?前面说了线程是占用 CPU 执行的基本单位,而 CPU 一般是使用时间片轮转方式让线程轮询占用的,所以当前线程 CPU 时间片用完后,要让出 CPU,等下次轮到自己时候在执行,那么如何知道之前程序执行到哪里了?其实程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行了。
另外每个线程有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其它线程是访问不了的,另外栈还用来存放线程的调用栈帧。
堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时候分配的,堆里面主要存放使用 new 操作创建的对象实例。
方法区则是用来存放进程中的代码片段的,是线程共享的。


二、线程生命周期

1、创建状态

当用 new 操作符创建一个新的线程对象时,该线程处于创建状态。
处于创建状态的线程只是一个空的线程对象,系统不为它分配资源。

2、可运行状态

执行线程的 start() 方法将为线程分配必须的系统资源,安排其运行,并调用线程体——run()方法,这样就使得该线程处于可运行状态(Runnable)。
这一状态并不是运行中状态(Running),因为线程也许实际上并未真正运行。

3、不可运行状态

当发生下列事件时,处于运行状态的线程会转入到不可运行状态:
    a、调用了 sleep() 方法;
    b、线程调用 wait() 方法等待特定条件的满足;
    c、线程输入/输出阻塞;
    d、返回可运行状态;
    e、处于睡眠状态的线程在指定的时间过去后;
    f、如果线程在等待某一条件,另一个对象必须通过 notify() 或 notifyAll() 方法通知等待线程条件的改变;
    g、如果线程是因为输入输出阻塞,等待输入输出完成。

三、线程优先级

1、线程优先级及设置

线程的优先级是为了在多线程环境中便于系统对线程的调度,优先级高的线程将优先执行。一个线程的优先级设置遵从以下原则:

    线程创建时,子继承父的优先级
    线程创建后,可通过调用 setPriority() 方法改变优先级;

    线程的优先级是1-10之间的正整数。


2、线程的调度策略

线程调度器选择优先级最高的线程运行。但是,如果发生以下情况,就会终止线程的运行:
    线程体中调用了 yield() 方法,让出了对 CPU 的占用权;
    线程体中调用了 sleep() 方法,使线程进入睡眠状态
    线程由于 I/O 操作而受阻塞;
    另一个更高优先级的线程出现;
    在支持时间片的系统中,该线程的时间片用完。


四、单线程的创建方法

单线程创建方式比较简单,一般只有两种方式:继承 Thread 类和实现 Runnable 接口;

需要注意的问题有:

    不管是继承 Thread 类还是实现 Runable 接口,业务逻辑是写在 run 方法里面,线程启动的时候是执行 start() 方法;
    开启新的线程,不影响主线程的代码执行顺序也不会阻塞主线程的执行;
    新的线程和主线程的代码执行顺序是不能够保证先后的;
    对于多线程程序,从微观上来讲某一时刻只有一个线程在工作,多线程目的是让 CPU 忙起来;

    通过查看 Thread 的源码可以看到,Thread 类是实现了 Runnable 接口的,所以这两种本质上来讲是一个;

1、继承Thread类:

package Thread1;

public class ThreadTest {
//
	public static class MyThread extends Thread{
		public void run() {
			System.out.println("继承Thread类实现线程创建");
		}
	}

	
	public static void main(String[] args) {
		new MyThread().start();
	}

}

如上代码 MyThread 类继承了 Thread 类,并重写了 run 方法,然后调用了线程的 start 方法启动了线程,当创建完 thread 对象后该线程并没有被启动执行。

当调用了 start 方法后才是真正启动了线程。其实当调用了 start 方法后线程 并没有马上执行而是处于就绪状态这个就绪状态是指该线程已经获取了除 CPU 资源外的其它资源,等获取 CPU 资源后才会真正处于运行状态。

run 方法执行完毕,该线程就处于终止状态了

使用继承方式好处是 run 方法内获取当前线程直接使用 this 就可以,无须使用 Thread.currentThread() 方法,不好的地方是:

Java 不支持多继承,如果继承了 Thread 类那么就不能再继承其它类,

另外任务与代码没有分离,当多个线程执行一样的任务时候需要多份任务代码

2、实现Runnable接口:

package Thread1;

public class RunableTest implements Runnable {
	

	public static void main(String[] args) {
		RunableTest r=new RunableTest();
		Thread t= new Thread(r);
		Thread t2=new Thread(r);
		t.start();
		t2.start();
	}

	@Override
	public void run() {
		System.out.println("使用Runnable接口实现多线程创建");
		
	}

}

如上面代码,两个线程公用一个 task 代码逻辑,需要的话 RunableTask 可以添加参数进行任务区分,另外 RunableTask 可以继承其他类,但是上面两种方法都有一个缺点就是任务没有返回值

3、FutureTask:有返回值

package Thread1;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest implements Callable {
	
	public static void main(String[] args) throws InterruptedException {
		CallableTest c1=new CallableTest();
		CallableTest c2=new CallableTest();
		FutureTask<String>futureTask =new FutureTask<String>(c1);
		FutureTask<String>futureTask1 =new FutureTask<String>(c2);	
		Thread t1=new Thread(futureTask);
		Thread t2=new Thread(futureTask1);
		t1.start();
		t2.start();
		try {
			String s=futureTask.get();
			String s2=futureTask1.get();
			System.out.println(s);
			System.out.println(s2);
		}catch(ExecutionException e){
			e.printStackTrace();
		}

	}

	@Override
	public Object call() throws Exception {
		return "hello";
	}

}

五、线程通知等待

Java 中 Object 类是所有类的父类,鉴于继承机制,Java 把所有类都需要的方法放到了 Object 类里面,其中就包含本节要讲的通知等待系列函数,这些通知等待函数是组成并发包中线程同步组件的基础

下面讲解下 Object 中关于线程同步的通知等待函数。

1、synchronized

同步机制 、 锁机制、 实现同步互斥,解决资源的访问冲突

//synchronized同步机制 、 锁机制、 实现同步互斥,解决资源的访问冲突
//1、synchronized 同步块
//2、synchronized 同步方法

package Thread1;

public class SynchronizedTest implements Runnable{
	int count=10;
	public synchronized void buy() {
		while(count>0) {
				if(count>0) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();					
					}
					System.out.println(Thread.currentThread().getName()+"购买倒数"+(count--)+"件商品");
				}
		}
	}
	
	public void run() {
		buy();//2、synchronized 同步方法
		/*while(count>0) {
			synchronized("") {   //1、synchronized 同步块
				if(count>0) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();					
					}
					System.out.println(Thread.currentThread().getName()+"购买倒数"+(count--)+"件商品");
				}
			}
		}*/
	}
	public static void main(String[] args) {
		System.out.println("开始执行");
		SynchronizedTest s=new SynchronizedTest();
		Thread t1=new Thread(s,"A");
		Thread t2=new Thread(s,"B");
		Thread t3=new Thread(s,"C");
		Thread t4=new Thread(s,"D");
		Thread t5=new Thread(s,"E");	
		Thread t6=new Thread(s,"F");
		Thread t7=new Thread(s,"G");
		t7.setPriority(10);
		t1.start();
		t2.start();
		t3.start();
		t4.start();		
		t5.start();
		t6.start();
		t7.start();	
		try {
			t7.join();
			t1.join();
			t2.join();
			t3.join();
			t4.join();
			t5.join();
			t6.join();		
		}catch(InterruptedException e) {
			e.printStackTrace();
		}

		
	}


}


2、join()

线程加入:线程A正在执行,如果此时想让线程B执行,并且要求线程B先于线程A执行完,则使用ThreadB.join()方法

//线程加入:线程A正在执行,如果此时想让线程B执行,并且要求线程B先于线程A执行完,则使用ThreadB.join()方法
//使用join()加入另一个线程时,另一个线程会等待被加入的线程执行完毕再执行自己

package Thread1;

public class JoinTest implements Runnable{
	@Override
	public void run() {
		System.out.println("A线程开始执行");
		for(int i=0;i<40;i++) {
			System.out.println("线程A"+i);
		}
		System.out.println("线程A运行结束");
	}
	public static void main(String[] args) {
		Thread  aThread=new Thread(new JoinTest());
		aThread.start();
		Thread bThread=new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("线程B开始执行");
				for(int i=2;i<10;i++) {
					System.out.println("线程B"+i);
				}
				System.out.println("线程B结束执行");
			}
			
		});
		bThread.start();
		try {
			bThread.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("main方法执行完毕");
	}



}

3、setPriority()

调度:就绪队列中的线程,被分配到处理器资源而进入运行状态之后,这个线程就车 称为被调度
java中:线程调度在优先级的基础上实行先到先服务的原则。
优先级分10级,1到10(int)

线程最终的执行顺序并不是预期的那样,优先级越高,越先执行,而是取决于哪个线程先取得系统资源。

//调度:就绪队列中的线程,被分配到处理器资源而进入运行状态之后,这个线程就车 称为被调度。
//java中:线程调度在优先级的基础上实行先到先服务的原则。
//优先级分10级,1到10(int)
//线程最终的执行顺序并不是预期的那样,优先级越高,越先执行,而是取决于哪个线程先取得系统资源。

package Thread1;

public class SetPriorityTest implements Runnable {

	public static void main(String[] args) {
		Thread thread1=new Thread(new SetPriorityTest(),"A");
		Thread thread2=new Thread(new SetPriorityTest(),"B");
		thread1.setPriority(3);
		thread2.setPriority(8);
		thread1.start();
		thread2.start();
	}

	@Override
	public void run() {
		for(int i=0;i<5;i++) {
			System.out.println("线程:"+Thread.currentThread().getName()+"第"+i+"次运行");
		}	
	}
	
}

4、sleep()

参数单位为:ms
//参数为:ms
package Thread1;

public class SleepTest implements Runnable{

	public static void main(String[] args) {
		Thread thread1=new Thread(new SleepTest(),"A");
		Thread thread2=new Thread(new SleepTest(),"B");
		Thread thread3=new Thread(new SleepTest(),"C");
		Thread thread4=new Thread(new SleepTest(),"D");
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
		
	}
	@Override
	public void run() {
		for(int i=1;i<=5;i++) {
			System.out.println("线程"+Thread.currentThread().getName()+i);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}

5、void wait() 方法

当一个线程调用一个共享对象(本文当讲到的共享对象就是共享资源)的 wait() 方法时候,调用线程会被阻塞挂起,直到下面几个事情之一发生才返回:
    其它线程调用了该共享对象的 notify() 或者 notifyAll() 方法;

    其它线程调用了该线程的 interrupt() 方法设置了该线程的中断标志,该线程会抛出 InterruptedException 异常返回。

另外需要注意的是如果调用 wait() 方法的线程没有事先获取到该对象的监视器锁,则调用 wait() 方法时候调用线程会抛出 IllegalMonitorStateException 异常。

6、void wait(long timeout) 方法

该方法相比 wait() 方法多一个超时参数,不同在于如果一个线程调用了共享对象的该方法挂起后,如果没有在指定的 timeout ms 时间内被其它线程调用该共享变量的 notify() 或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。

需要注意的是如果在调用该函数时候 timeout 传递了负数会抛出 IllegalArgumentException 异常。

7、void wait(long timeout, int nanos) 方法

 public final void wait1(long timeout, int nanos) throws InterruptedException {        
      if (timeout < 0) {            
			 throw new IllegalArgumentException("timeout value is negative");
     }        if (nanos < 0 || nanos > 999999) {            
    	 throw new IllegalArgumentException(  "nanosecond timeout value out of range");
     }        if (nanos > 0) {
         timeout++;
     }

     wait(timeout);
 }

8、void notify() 方法

一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程,一个共享变量上可能会有多个线程在等待,具体唤醒哪一个等待的线程是随机的。


另外被唤醒的线程不能马上从 wait 返回继续执行,它必须获取了共享对象的监视器后才可以返回,也就是唤醒它的线程释放了共享变量上面的监视器锁后,被唤醒它的线程也不一定会获取到共享对象的监视器,这是因为该线程还需要和其它线程一块竞争该锁,只有该线程竞争到了该共享变量的监视器后才可以继续执行

类似 wait 系列方法,只有当前线程已经获取到了该共享变量的监视器锁后,才可以调用该共享变量的 notify() 方法,否者会抛出 IllegalMonitorStateException 异常。

9、void notifyAll() 方法

package Thread1;

public class Notify_NotifyAll_Test {
	private static volatile Object resourceA=new Object();
	
	public static void main(String[] args) throws InterruptedException {
		Thread threadA=new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized(resourceA) {
					System.out.println("threadA get resourceA lock");
					System.out.println("threadA begin wait");
					try {
						resourceA.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("threadA end wait");
				}
			}
			
		});
		
		
		Thread threadB=new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized(resourceA) {
					System.out.println("threadB get resourceA lock");
					System.out.println("threadB begin wait");
					try {
						resourceA.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("threadB end wait");
				}
			}
			
		});
		
		
		Thread threadC=new Thread(new Runnable() {

			@Override
			public void run() {
				synchronized(resourceA) {
					System.out.println("threadC begin notify");
					resourceA.notifyAll();
				}
			}		
		});
		
		threadA.start();
		threadB.start();
		Thread.sleep(1000);
		threadC.start();
		threadA.join();
		threadB.join();
		threadC.join();
		System.out.println("main over");
		
	}

}

六、为什么要用线程池

通过上面的介绍,完全可以开发一个多线程的程序,为什么还要引入线程池呢。主要是因为上述单线程方式存在以下几个问题:

    线程的工作周期:线程创建所需时间为 T1,线程执行任务所需时间为 T2,线程销毁所需时间为 T3,往往是 T1+T3 大于 T2,所有如果频繁创建线程会损耗过多额外的时间;

    如果有任务来了,再去创建线程的话效率比较低,如果从一个池子中可以直接获取可用的线程,那效率会有所提高。所以线程池省去了任务过来,要先创建线程再去执行的过程,节省了时间,提升了效率;

    线程池可以管理和控制线程,因为线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控;

    线程池提供队列,存放缓冲等待执行的任务。

大致总结了上述的几个原因,所以可以得出一个结论就是在平时工作中,如果要开发多线程程序,尽量要使用线程池的方式来创建和管理线程。

通过线程池创建线程从调用 API 角度来说分为两种,一种是原生的线程池,另外该一种是通过 Java 提供的并发包来创建,后者比较简单,后者其实是对原生的线程池创建方式做了一次简化包装,让调用者使用起来更方便,但道理都是一样的。所以搞明白原生线程池的原理是非常重要的。

七、ThreadPoolExecutor

构造函数:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue); 

1、corePoolSize 核心池的大小

在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads() 或者 prestartCoreThread() 方法,从这两个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建 corePoolSize 个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。

2、maximumPoolSize

线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程。

3、keepAliveTime

表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。

但是如果调用了 allowCoreThreadTimeOut(boolean) 方法,在线程池中的线程数不大于 corePoolSize 时,keepAliveTime 参数也会起作用,直到线程池中的线程数为0。

4、unit

参数 keepAliveTime 的时间单位。

5、workQueue

一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下这几种选择:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。

6、threadFactory

线程工厂,主要用来创建线程

7、hander

表示当拒绝处理任务时的策略,有以下四种取值:
    ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常;
    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常;
    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程);

    ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

8、线程池之间的参数协作

简单总结下线程池之间的参数协作分为以下几步:
    线程优先向 CorePool 中提交;
    在 Corepool 满了之后,线程被提交到任务队列,等待线程池空闲;

    在任务队列满了之后 corePool 还没有空闲,那么任务将被提交到 maxPool 中,如果 MaxPool 满了之后执行 task 拒绝策略。




9、以上就是原生线程池创建的核心原理。除了原生线程池之外并发包还提供了简单的创建方式,上面也说了它们是对原生线程池的一种包装,可以让开发者简单快捷的创建所需要的线程池。


八、Executors

1、newSingleThreadExecutor

创建一个线程的线程池,在这个线程池中始终只有一个线程存在。如果线程池中的线程因为异常问题退出,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2、newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3、newCachedThreadPool

可根据实际情况,调整线程数量的线程池,线程池中的线程数量不确定,如果有空闲线程会优先选择空闲线程,如果没有空闲线程并且此时有任务提交会创建新的线程。在正常开发中并不推荐这个线程池,因为在极端情况下,会因为 newCachedThreadPool 创建过多线程而耗尽 CPU 和内存资源。

4、newScheduledThreadPool

可根据实际情况,调整线程数量的线程池,线程池中的线程数量不确定,如果有空闲线程会优先选择空闲线程,如果没有空闲线程并且此时有任务提交会创建新的线程。在正常开发中并不推荐这个线程池,因为在极端情况下,会因为 newCachedThreadPool 创建过多线程而耗尽 CPU 和内存资源。

5、newScheduledThreadPool

此线程池可以指定固定数量的线程来周期性的去执行。比如通过 scheduleAtFixedRate 或者 scheduleWithFixedDelay 来指定周期时间。

PS:另外在写定时任务时(如果不用 Quartz 框架),最好采用这种线程池来做,因为它可以保证里面始终是存在活的线程的。


九、推荐使用 ThreadPoolExecutor 方式


在阿里的 Java 开发手册时有一条是不推荐使用 Executors 去创建,而是推荐去使用 ThreadPoolExecutor 来创建线程池。
这样做的目的主要原因是:使用 Executors 创建线程池不会传入核心参数,而是采用的默认值,这样的话我们往往会忽略掉里面参数的含义,如果业务场景要求比较苛刻的话,存在资源耗尽的风险;另外采用 ThreadPoolExecutor 的方式可以让我们更加清楚地了解线程池的运行规则,不管是面试还是对技术成长都有莫大的好处。


十、参考文献:

<<java开发课堂实录>>

java多线程


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值