1、线程池简介
1.1 线程池的概念:
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
1.2 线程池的工作机制
-
在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
-
一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
1.3 使用线程池的好处
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处:
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
2、JDK对线程池的支持
JDK提供了Executor框架,可以让我们有效的管理和控制我们的线程,其实质也就是一个线程池。Executor下的接口和类继承关系如下:
其中,Executors相当于是一个工具类,提供了一系列静态工厂方法用于创建各种线程池:
挑几个比较重要的来看一下:
-
newFixedThreadPool:该方法返回一个固定线程数量的线程池;
-
newSingleThreadExecutor:该方法返回一个只有一个现成的线程池;
-
newCachedThreadPool:返回一个可以根据实际情况调整线程数量的线程池;
-
newSingleThreadScheduledExecutor:该方法和newSingleThreadExecutor的区别是给定了时间执行某任务的功能,可以进行定时执行等;
-
newScheduledThreadPool:在4的基础上可以指定线程数量。
创建线程池实质调用的还是ThreadPoolExecutor
在Executors类中,我们拿出来一个方法简单分析一下:
可以看出,类似的其他方法一样,在Executors内部创建线程池的时候,实际创建的都是一个ThreadPoolExecutor对象,只是对ThreadPoolExecutor构造方法,进行了默认值的设定。ThreadPoolExecutor的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
....
}
方法的内容略看,仔细分析一下它的参数:
- corePoolSize :核心线程池大小,也就是默认的工作线程数量;
- maximumPoolSize : 线程池最大容量大小;
- keepAliveTime :线程池空闲时,线程存活的时间;
- TimeUnit :上一个参数的时间单位;
- ThreadFactory :线程工厂,一般使用默认的 ;
- BlockingQueue:阻塞任务队列,用于存放提交来的任务;
- RejectedExecutionHandler: 线程拒绝策略;
尝试使用一下线程池:
public static void threadPoolTest() {
//ExecutorService threadPool = Executors.newFixedThreadPool(5);
//ExecutorService threadPool = Executors.newSingleThreadExecutor();
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 1;i<=10;i++){
threadPool.execute(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
threadPool.shutdown();
}
3、手写线程池
参考阿里巴巴java开发手册:
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
手写一个线程池:(不考虑拒绝策略)
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executor = new ThreadPoolExecutor(
2,
6,
2,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory()
);
for (int i = 1;i<5;i++){
executor.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
executor.shutdown();
}}
其实线程提交可以使用execute和submit这两种方法,但是两者有所不同:
(1)execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute()
方法输入的任务是一个Runnable类的实例。
(2)submit()
方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
4、线程池的核心原理
先从一张看起,详细描述了一个任务的执行情况和线程池的工作过程:
我们来解释一下整个流程:
1)在创建了线程池之后等待提交任务请求过来;
2)当调用了execute()方法提交一个任务请求的时候,线程池会执行以下的判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程执行这个任务;
- 如果正在运行的线程数量大于等于corePoolSize,那么将这个任务放入任务队列中;
- 如果此时任务队列已经满了,且正在运行的线程小于maxnumPoolSize,那么还是要创建非核心线程去执行任务;
- 如果队列已经满了,且正在运行的线程数量已经达到了最大maxnumPoolSize,那么线程池会启动饱和拒绝策略来执行;
3)当一个线程完成任务的时候,它会从队列中取出下一个任务来执行;
4)当一个线程无事可做超过keepAliveTime时,线程会停掉大于corePoolSize数量的线程。
5、JDK内置拒绝策略
什么是拒绝策略呢?当等待队列也已经排满了,再也塞不下新的任务了同时,线程池的max也到达了,无法接续为新任务服务,这时我们需要拒绝策略机制合理的处理这个问题。
JDK内置了四种常用的拒绝策略:
- AbortPolicy:它是默认的拒绝策略,直接抛出RejectedException异常阻止系统正常的运行;
- CallerRunPolicy:调用者运行策略,该策略不会抛弃任务,也不会抛出异常,而是将任务返回给调用者执行,比如main线程提交的任务,就会被返回给main线程执行;
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中再次尝试提交;
- DiscardPolicy:直接丢弃任务,不予处理,不抛出异常。
参考资料: