1、介绍
什么是线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
具有如下特征:
- 1、线程池线程都是后台线程。
- 2、每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
- 3、如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。
- 4、如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。
- 5、超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
简而言之,线程池是基于池化技术,运行队列实现线程的高可用和可控性,从而提高多线程的性能和稳定性以及准确性。
使用线程池的优点和缺点
线程池是一种管理线程从而更高效使用线程的策略。
使用线程池的好处:
- 1、降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 2、提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。
- 3、提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优。
什么时候需要线程池
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。 当T1+T3 远远大于T2 ,就可以使用线程池,从而合理的利用到这些闲暇时间。
2、工作原理
java中线程池的类图
线程池用起来比较简单,但其内部就像一台仪器,需要各个部分相互协作来完成相应的功能。设计模式就用了工厂模式、策略模式、代理模式以及模板模式。ThreadPoolExecutor类图如下:
-
1、DefaultThreadFactory是线程工厂,实现了ThreadFactory,其中newThread()方法是创建线程的方法,这里使用了工厂模式,封装了线程的创建过程。
poolNumber用来统计线程工厂的个数,同时也是线程池名称的一部分;
threadNumber用来记录每个线程工厂创建了多少个线程,是线程名称的一部分。 -
2、Worker继承了AQS,实现了Runnable接口,这里使用了代理模式,对任务线程的run()方法进行了加强。WorKer继承了AQS,自己实现了简单的不可重入锁,其中state=0表示锁未被获取状态,state=1表示锁已经被获取的状态,state=-1是创建Worker时默认的状态,创建时设置为-1是为了避免该线程在运行 runWorker()方法前被中断。
firstTast:记录了该工作线程执行的第一个任务
thread:是需要执行的任务线程
RejectedExecutionHandler:饱和策略,当队列满并且线程个数达到maximumPoolSize后采取的策略,这里使用了策略模式。
AbortPolicy:抛出异常(默认情况下使用该策略)
CallerRunsPolicy:使用调用者所在线程运行任务
DiscardOldestPolicy:调用poll弹出BlockingQueue的队头元素,执行当前任务
DiscardPolicy:默默丢弃,抛出异常 -
3、ThreadPoolExecutor继承了AbstractExecutorService,AbstractExecutorService中的submit()方法使用了模板模式。重要属性其含义如下。
corePoolSize:线程池核心线程数
maximumPoolSize:线程池的最大线程数
workQueue:阻塞队列,用来存放任务线程
threadFactory:线程工厂,用来创建线程
handler:饱和策略,默认情况下使用AbortPolicy
keepAliveTime:存活时间。如果线程池中的线程数量比核心线程数量多,并且是闲置状态,这些线程的最大存活时间
TimeUnit:存活时间的单位
clt:用来记录线程池中线程的个数和线程池的状态。线程池通过线程池的状态来控制任务的执行。
线程池的执行过程
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面
有任务,线程池也不会马上执行它们。 - 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
会抛出异常 RejectExecutionException。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
最终会收缩到 corePoolSize 的大小
线程池的状态
- RUNNING:接受新任务并处理阻塞队列中的任务线程
- SHUTDOWN:拒绝新任务但是处理阻塞队列中的任务
- STOP:拒绝新任务并抛弃阻塞队列中的任务,同时中断正在处理的任务
- TIDYING:所有任务执行完(包含阻塞队列里的任务)后,当前线程池活动线程数为0,将调用terminated()方法
- TERMINATED:终止状态,调用terminated方法执行完后的状态。
线程池的状态转换
- RUNNING -->SHUTDOWN:显示调用shutdown()方法,获取隐式调用finalize()方法里面的shutdown()方法
- RUNNING或SHUTDOWN)-->STOP:显示调用shutdownNow()方法
- SHUTDOWN --> TIDYING :当线程池和任务队列都为空时
- STOP-->TIDYING:当线程池为空时
- TIDYING -->TERMINATED:当terminated()方法执行后
线程池大小
1、CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响。一旦任务停止,CPU就会出于空闲状态,而这种情况下多出来一个线程就可以充分利用CPU的空闲时间。
2、I/O密集型(2N):这种任务应用起来,系统大部分时间用来处理I/O交互,而线程在处理I/O的是时间段内不会占用CPU来处理,这时就可以将CPU交出给其他线程使用。因此在I/O密集型任务的应用中,可以配置多一些线程,具体计算方是2N。
3、Executors
Executors常用方法
Java通过Executors提供四种线程池,分别为:
1、newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2、newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
4、newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
5、newWorkStealingPool 使用所有可用的处理器作为其目标并行级别来创建工作线程池
其中newWorkStealingPool来自1.8。其余来自1.5。
Executors存在什么问题
如此完美的线程池静态工厂类,为什么阿里巴巴Java开发手册中明确指出,而且用的词是『不允许』使用Executors创建线程池。阿里禁止使用Executors
首先,Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。
ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。问题就出现在这
不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。
上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,就有可能导致OOM。
4、最佳实践
避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。
当然还有一些开源组件可以使用。