线程池复习笔记
1. 线程池状态:
runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
2. largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系。
3. 比较重要成员变量:
private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集
private volatile long keepAliveTime; //线程存活时间
private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间
private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int maximumPoolSize; //线程池最大能容忍的线程数
private volatile int poolSize; //线程池中当前的线程数
private volatile RejectedExecutionHandler handler; //任务拒绝策略
private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程
private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount; //用来记录已经执行完毕的任务个数
1. 线程池状态:
runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;
状态值:RUNNING,SHUTDOWN,STOP,TERMINATED
2. 任务的执行:
1). 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
2). 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),
则会尝试创建新的线程去执行这个任务;
3). 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
4). 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,
那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
3. 线程池中的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
prestartCoreThread():初始化一个核心线程;
prestartAllCoreThreads():初始化所有核心线程
4. 任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue<Runnable>,通常可以取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
newFixedThreadPool,newSingleThreadExecutor线程池任务队列的实现。当线程池中没有可用线程时,新提交的任务将被加入到工作队列,等待有可用的空闲线程。
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
newCachedThreadPool任务队列的实现,将任务直接提交而不保存,SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,因此线程池中如果不存在可用于直线任务的空闲线程,
则新创建线程并运行。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个
线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue[3]。
5. 任务拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 默认处理策略。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
6. 线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
7. 线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
8. 线程池生成方式:
在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:
1). Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE;newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,
使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue());
}
该线程池corePoolSize = 0, maximunPoolSize = Integer.MAX_VALUE,说明开始创建线程池后,并不会预创建线程,线程池最大线程数为Integer.MAX_VALUE,理论上可以拥有如此大的线程数。keepAliveTime = 60L,
单位为Seconds,空闲线程会在等待60s之后被线程池回收掉。阻塞队列采用了SynchronousQueue,即有任务时会查询是否有空闲线程,如果有则自动执行任务,没有则创建新的线程,并执行任务。
适用情形:适用于高并发,短任务的情形。线程使用完毕并不会立即回收线程,在长时间空闲时会自动回收线程。
2). Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池,newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue;
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()));
}
类似于newFixedThreadPool,nThreads = 1,线程池中只能运行一个线程,后续的任务将被加入到阻塞队列。
适用情形:只能有一个运行的线程。
3). Executors.newFixedThreadPool(int); //创建固定容量大小的缓冲池,newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
}
可以看出该方式中corePoolSize = maximumPoolSize = nThreads,即newFixedThreadPool一旦创建,则预先创建nThreads个线程并启动,如果后续任务加入进来时,有空闲线程,则使用,否则将任务加入到阻塞队列,
等待可用线程。keepAliveTime = 0,线程池不会回收空闲线程。还有一个重载版本newFixedThreadPool(int nThreads, ThreadFactory threadFactory),可以自定义线程工厂。
适用情形:固定的线程数量,预先创建线程,不会回收线程,抵消了创建/销毁线程所带来的额外开销,适用于稳定数量的并发线程。
4). newWorkStealingPool:
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool(parallelism,ForkJoinPool.defaultForkJoinWorkerThreadFactory,null, true);
}
该线程池使用ForkJoinPool实现,参数parallelism为线程的并行数量其继承自AbstractExecutorService。ForkJoinPool实现了一个工作窃取算法,使得空闲线程能够窃取别的线程分解出来的子任务,
从而让所有的线程都尽可能处于满负荷,提高执行效率。有一个无参数版本,其将parallelism设为Runtime.getRuntime().availableProcessors(),即处理器核数。
ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。这种思想值得学习。
ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。
子类重写 compute 方法。
//并行执行两个 小任务
left.fork();
right.fork();
//把两个小任务累加的结果合并起来
left.join()+right.join();
参见:https://www.cnblogs.com/lixuwu/p/7979480.html
5). newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
通过ScheduledThreadPoolExecutor实现,通过corePoolSize指定返回一个ScheduledExecutorService,可以通过设置执行参数而指定任务延迟启动时间和执行周期。
我们通过查看ScheduledThreadPoolExecutor的源代码,可以发现ScheduledThreadPoolExecutor的构造器都是调用父类的构造器,只是它使用的工作队列是java.util.concurrent.ScheduledThreadPoolExecutor.DelayedWorkQueue
通过名字我们都可以猜到这个是一个延时工作队列.实际中,如果Executors提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置ThreadPoolExecutor的参数有点麻烦,要根据实际任务的类型
和数量来进行配置。另外,如果ThreadPoolExecutor达不到要求,可以自己继承ThreadPoolExecutor类进行重写。
9. 通过创建线程池返回的ExecutorService可以执行任务,执行任务主要有以下种方法:
1). void execute(Runnable command) 执行一个实现了Runnable接口的任务,没有返回值。
2). Future<?> submit(Runnable task) / Future submit(Runnable task, T result) / Future submit(Callable task):方法中的参数Callable类似于Runnable,只是在Callable接口可以返回任务的执行结果,而Runnable是没有返回值。
这组任务执行提交方法提供了一个异步的Future返回值,可以通过Future去获得任务的返回值,并进行异步控制。通过Future中的isDown()可以判断任务是否结束,还可以通过cancel取消任务,通过isCancel判断任务是否取消,
通过get方法将会使get所在的线程一直阻塞到任务结束获取结果。
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(() -> {
System.out.println("Runnable task");
})
Future result1 = exec.submit(() -> {
System.out.println("Callable task");
return 2 * 3;
});
result1.get();
3). T invokeAny(Collection<? extends Callable> tasks) / T invokeAny(Collection<? extends Callable> tasks,long timeout, TimeUnit unit)
这一组方法是批量提交任务的方法,其中invokeAny方法:阻塞主线程直到有一个任务执行完毕,并自动取消剩余正在执行的任务,返回执行完毕任务的结果。long time提供了一个等待超时参数。
4). invokeAll(Collection<? extends Callable> tasks) / List<Future> invokeAll(Collection<? extends Callable> tasks,long timeout, TimeUnit unit)
invokeAll方法类似于invookeAny方法,区别在于:invokeAll阻塞主线程直到所有任务执行完毕,并返回任务执行Future集合。同样long time提供了一个超时参数。
5). schedule / scheduleAtFixedRate / scheduleWithFixedRate
a).schedule(Callable callable, long delay, TimeUnit unit):将任务延迟delay时间执行,使用Callable接口。
b).scheduleAtFixedRate(Runnable command, long initalDelay, long period, TimeUnit unit):该方法会将任务延迟initalDelay时间并循环执行,周期为period,下个执行时间点为从任务启动执行后period时间。同样TimeUnit为时间单位。
c).scheduleWithFixedRate(Runnable command, long initalDelay, long period, TimeUnit unit):类似于上一个方法,但是这里任务延迟开始执行后,下个任务周期执行时间是从上个任务结束时算起。
public class Test { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5)); for(int i=0;i<15;i++){ MyTask myTask = new MyTask(i); executor.execute(myTask); System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+ executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount()); } executor.shutdown(); } } class MyTask implements Runnable { private int taskNum; public MyTask(int num) { this.taskNum = num; } @Override public void run() { System.out.println("正在执行task "+taskNum); try { Thread.currentThread().sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("task "+taskNum+"执行完毕"); } }
思想:
1. shutdown() 和 shutdownNow(),通过一个状态值来标记状态。
2. corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。
3. 这里有一个非常巧妙的设计方式,假如我们来设计线程池,可能会有一个任务分派线程,当发现有线程空闲时,就从任务缓存队列中取一个任务交给空闲线程执行。但是在这里,并没有采用这样的方式,
因为这样会要额外地对任务分派线程进行管理,无形地会增加难度和复杂度,这里直接让执行完任务的线程去任务缓存队列里面取任务来执行。
4. 先不加锁判断,如果真的需要改变的时候,再加锁,再判断一次,再改变
try {} 和 finally {}一起使用,可以不需要 catch(){}