先来谈谈线程池
了解线程池之前,你有哪些熟知的线程池有哪些呢?
String(字符串常量池),MySQL JDBC,数据库连接池(DataSource);通过对他们的了解,大概知道了出现池的主要目的——减少不必要的开销,提高效率;
为什么出现了线程池呢?
线程出现的目的是因为进程太重量级了,导致创建线程或者销毁进程效率很低,而线程就是为了资源共享,新的线程复用之前的资源,就提高了效率,但是如果线程创建/销毁 的速率非常高,那么线程的创建/销毁带来的开销就是不可忽略的;
线程池的基本原理是什么?
这就像DataSource一样,再建立连接之后,同时也会保留之前的一些连接,后面若在需要建立连接,直接从池子中取就OK,这样也就减少了重新建立连接的开销;
造一个池子,里面创建很多线程,当再次需要执行任务的时候,就不用再创建线程了,而是直接从池子中取出一个现成的线程供使用,即使该线程完成了任务,也不销毁线程,而是继续呆在线程池里准备迎接下一个任务;
为什么从池子中取,要比创建线程快?
创建线程的确需要申请一点资源,但这已经很少,很快了~但是,创建线程,是要在操作系统内核中完成的,涉及到用户态向内核态的切换操作,这个操作需要一定的开销;应用程序创建线程的是需要通过系统调用来完成的,进入操作系统内核中执行,也就是说,线程本质上就是PCB,是内核中的数据结构;
所以这个过程大概是这样的:引用程序发起创建线程的行为,内核接到指令,在内核中完成PCB的创建,再把PCB加入调度队列中,最后返回给应用程序;(如下图)
所以创建线程,是在内核中完成的,需要经历 用户态->内核态的转变,而从线程池中取线程,把线程放回线程池,这一套操作是纯用户态的逻辑
线程池有什么优点?
- 减少线程创建和销毁带来性能上的开销
- 提高效率,当任务来时,可以直接使用线程,不用等待创建
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。使用线程池可以进行统一的分配,调优和监控。
谈谈newCachedThreadPool
在Java中,线程池的本体叫ThreadPoolExecutor,他的构造方法写起来十分麻烦,为了简化构造方法,标准库就提供了一系列工厂方法,简化使用;
什么是工厂模式?
new的过程中,就需要调用构造方法,有时候希望能够提供多种构造实例的方法,就需要重载构造方法来实现不同版本的实例创建,但是重载要求参数个数/类型不同,就带来了一定的限制;构造方法存在一定的局限性,为了围绕局限,就引入了工厂模式
工厂模式,将创建产品实例的权利移交工厂,我们不再通过new来创建我们所需的对象,而是通过工厂来获取我们需要的产品。降低了产品使用者与使用者之间的耦合关系;
不明白?来看看下图:
这里创建线程池就没有显式new,而是通过Executors这个静态方法newCaChedThreadPool来完成的;
常见用法:
线程池的单纯使用很简单,使用submit方法,把任务提交到线程池中即可,线程池中会有线程来完成这些任务;(如下图)
实现简易线程池
想要模拟实现一个简单的线程池,就要先弄清线程池基本特性:一个线程池可以同时提交N个任务,对应的线程池中则有M个线程来完成这N个任务;
如何把N个任务分配给M个线程呢?
生产者消费者模型~
这就需要一个阻塞队列,将提交的任务都放入队列中,创建M个线程来从队列中取元素,若队列为空,就阻塞等待新任务加入队列,若队列不为空,每个线程都去取任务,然后执行,执行完当前任务后,继续取下一个...直到队列为空,继续阻塞等待;
代码如下:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
//线程池
class MyThreadPool {
//阻塞队列存放任务
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//存放任务
public void submit(Runnable task) throws InterruptedException {
queue.put(task);
}
//创建vip个线程来执行任务
public MyThreadPool(int vip) {
for(int i = 0; i < vip; i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
Runnable task = queue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
}
}
}
标准库中的ThreadPoolExecutor
在Java的标准库中提供的ThreadPoolExecutor要复杂的多,构造方法可以支持很多参数,可以支持很多选项,来创造出不同风格的线程池;
线程池的主要参数:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler)
注意:黄字为比方,更方便理解
参数 | 解释 |
---|---|
corePoolSize |
线程池中的核心线程数。当有请求任务来之后,若线程池已创建的线程数小于corePoolSize,无论当前线程池中是否有线程,都会创建一个新线程来执行该任务。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中,之后就会从队列中继续取任务. 比方一个公司的正式员工,即使摸鱼也不会被开除的那种(主要线程空闲了也不会被销毁) |
maximumPoolSize |
线程池允许的最大线程个数;当队列满了,并且创建的线程数小于maximumPoolSize,则线程池会创建新的线程来完成任务; 比方一个公司的实习员工,摸鱼被发现了,就会被开除(非主要线程,空闲时间达到一定时间,就会被销毁) |
keepAliveTime |
空闲线程的存活时间。当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。 比方一个公司的实习员工的最大摸鱼时间,相当于给实习生摸鱼的一个时间上线; |
unit | keepAIiveTime的单位。 |
workQueue |
任务队列。用于传输和保存等待执行任务的阻塞队列; (可以手动给线程池传入一个任务队列,线程池本来也有自己的任务队列,如果不传,也会自己内部创建) |
threadFactory | 线程工厂。用于创建新线程。描述了线程是如何创建的,工厂对象就负责创建线程,程序员可以手动指定线程创建策略 |
handler(重点) | 线程拒绝策略。当线程池的线程和队列都满了(工作线程忙不过来了),若有人继续往里面添加新任务,就会执行该策略 |
当线程池中的线程数目小于核心线程数目,此时,有新的任务来临,会继续创建线程么?为什么?
会继续创建线程,因为当线程池中的线程数目小于核心线程数目时,说明此时系统资源并不紧张,可以继续创建线程来执行任务,并为下一次接受更多的任务提前做好准备,防止在系统资源紧张,并且还有大量任务来临的情况下,急急忙忙创建线程,这时候就是为本就紧张的系统资源上雪上加霜~
打个比方,这就像是考试的提前一个月做好复习工作,和你考试前3天做好复习工作一样,考试前一个月,你有充足的时间准备各科考试的复习(提前创建好线程),而考试前3天复习,要么就复习好一科,要么就各科都复习不好.
拒绝策略:(重点)注意:括号内为比方,更方便理解
-
-
static class
ThreadPoolExecutor.AbortPolicy
被拒绝的任务的处理程序,抛出一个
RejectedExecutionException
。(好比领导给员工安排了很多活,加班都干不完,这个员工干不下去了,一瞬间就情绪崩溃,晕倒了;)
static class
ThreadPoolExecutor.CallerRunsPolicy
一个被拒绝的任务的处理程序,直接在
execute
方法的调用线程中运行被拒绝的任务,除非执行程序已经被关闭,否则这个任务被丢弃。( 好比领导给员工安排了很多任务,员工干不过来,员工就给领导说,你来干吧,这时候领导自己干,如果能干他就干了,不能干,就丢弃这个任务了;)
static class
ThreadPoolExecutor.DiscardOldestPolicy
被拒绝的任务的处理程序,丢弃最旧的未处理请求,然后重试
execute
,除非执行程序关闭,在这种情况下,任务被丢弃。(好比领导个员工安排了任务1、任务2、任务3,领导安排到任务4的时候,员工说谈干不了,领导就说,没事,任务1不着急,你先干任务4;)
static class
ThreadPoolExecutor.DiscardPolicy
被拒绝的任务的处理程序静默地丢弃被拒绝的任务。
(好比领导个员工安排了任务1、任务2、任务3,领导安排到任务4的时候,员工说谈干不了,领导说,没事,任务4不着急,你先干之前的吧;)
-
线程池的线程数目如何确定?设定成几合适?(最易错!)
这里是不能给出具体个数的,这里面试官考察的关键是——如何设置线程数目的方法(实验+测试)
线程池的线程数目无法确定具体数目,为什么呢?
- 主机的CPU的配置不确定
- 你的程序执行特点不确定
你的代码里具体干了什么?是CPU密集型的任务(做了大量的算数运算和逻辑运算),还是IO密集型的任务(做了大量的读写网卡或者读写硬盘),还有的程序既需要进行很多的CPU密集型任务,又需要进行很多的IO任务,所以在实际的开发中,很难量化两种任务的比例;
面临开发问题如何解决:如果任务全都是CPU密集型,线程数目最多也就是N,再多也没有意义,因为CPU已经占满了;但如果任务中10%是CPU密集型,90%是IO,线程数目设置成10N也没有关系;
测试方法:实际开发处理方案是需要实验验证的!针对你的程序性能测试,分别给线程池设置成不同的数目:0.5N、N、1.5N、2N...都可以试试,然后分别记录每种情况下你的程序的一些核心性能指标和系统负载情况,最后选择一个合适的配置
补充几种常考的线程池
1)newSingleThreadExecutor
单线程化线程池(newSingleThreadExecutor)的优点,串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它(此线程池保证所有任务的执行顺序按照任务的提交顺序执行)。
Ps:因为是单线程池,我使用.shutdown()方法关闭线程池,这里不会出错,因为我是先把任务放进线程池,然后在关闭线程池,shutdown()方法会让正在执行的任务继续执行下去,没有被执行的被中断。
曾经遇到过一次面试的时候问过:既然new Thread()和newSingleThreadExecutor()都是创建一个线程处理,为什么还需要存在单个线程的线程池呢?
当手动去操作查看线程名称编号的时候就会发现:
- new Thead()的方式每次都会新建一个线程来处理,增加消耗;
- newSingleThreadExecutor()的方式只使用同一个线程来处理,减少了消耗;
2)newFixedTheadPool
newFixedTheadPool是六种常用线程池的其中一种,newFixedThreadPool的特点是他的核心线程数和最大线程数是一致的,并且是一个固定线程数的线程池。线程池的大小一旦达到最大值后,再有新的任务提交时则放入无界阻塞队列中,等到有线程空闲时,再从队列中取出任务继续执行。
Ps:使用完成,需要手动关闭线程池