Java线程池入门
什么是线程池
我们知道线程创建的多种方式:
- 继承Thread
- 实现Runnable
- 实现Callable
线程池就是创建一个容器,来管理我们的线程,将线程的创建过程交给线程池,我们只需要调用线程池的execute()方法,将需要执行的任务(Runnable的实现类)传进去,线程池就能帮我们完成任务。
为什么要使用线程池
这是我们熟悉的任务管理器,我们打开性能可以看到,目前电脑上有194个进程和3214个线程,其中的线程就是指我们的内核线程
执行普通的方法执行线程10000个线程
public class ThreadPoolDemo1 {
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(ThreadPoolDemo1::run).start();
}
}
public static void run(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们看到,系统线程飙升到13262和之前的3214相比高了10000不止(JVM启动时也会有一定的线程启动)。
实际上过了两秒钟又恢复到3000,程序挂了。
使用线程池
我们使用最简单方式启动一个长度为10的定长线程池,我们启动一下。
public class ThreadPoolDemo1 {
public static void main(String[] args) {
Executor executor=Executors.newFixedThreadPool(10);
for (int i = 0; i < 10000; i++) {
executor.execute(ThreadPoolDemo1::run);
}
}
public static void run(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们看到线程变化不大,和之前直接飙升到10000+天壤之别。
首先我们科普两个概念,用户线程和内核线程。我们的用户进程(可以理解为Thread类)不能直接被操作系统直接执行,需要转换为内核线程,然而这个过程比较耗时,这也是线程池出现的原因。
上述程序我们看到,使用线程池可以有效地限制线程的产生。
用户线程与内核线程
线程池的基本思想
首先还是刚刚的例子,我们知道Thread.currentThread().getName()方法可以打印当前正在执行的线程的线程名字,我们从执行结果找找答案:
普通的方法执行20个线程
public class ThreadPoolDemo1 {
public static void main(String[] args) {
Executor executor=Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
new Thread(ThreadPoolDemo1::run).start();
//executor.execute(ThreadPoolDemo1::run);
}
}
//打印当前线程名
public static void run(){
System.out.println(Thread.currentThread().getName());
}
}
结果:
Thread-2
Thread-3
Thread-6
Thread-7
Thread-0
Thread-1
Thread-10
Thread-4
Thread-5
Thread-16
Thread-11
Thread-14
Thread-15
Thread-18
Thread-19
Thread-12
Thread-13
Thread-8
Thread-17
Thread-9
使用线程池执行20个线程
public class ThreadPoolDemo1 {
public static void main(String[] args) {
Executor executor=Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executor.execute(ThreadPoolDemo1::run);
}
}
//打印当前线程名
public static void run(){
System.out.println(Thread.currentThread().getName());
}
}
结果
pool-1-thread-2
pool-1-thread-5
pool-1-thread-3
pool-1-thread-6
pool-1-thread-4
pool-1-thread-1
pool-1-thread-7
pool-1-thread-7
pool-1-thread-9
pool-1-thread-8
pool-1-thread-10
pool-1-thread-2
pool-1-thread-6
pool-1-thread-7
pool-1-thread-5
pool-1-thread-3
pool-1-thread-10
pool-1-thread-6
pool-1-thread-1
pool-1-thread-4
区别:
我们看到普通的方式就是创建20个线程,默认名是Thread加个数,从1开始。
线程池的name被修改过,并且我们可以看到pool-1-thread-1,pool-1-thread-2都有执行两次任务。
所以线程池简单的说就是创建一个线程多次使用,由于用户线程转换为内核线程劳民伤财,不如我们创建好之后反复使用,这就是线程池的基本思想
线程池的执行流程
我们看一下这个图,然后我一一解释图中的各个部分:
简单流程
-
首先我们要执行一个任务,先尝试交给线程池执行,线程池有一定的容量,图中是5。
-
如果线程池中的工作线程都在执行任务,那么我们就将任务加入到图中的队列中去等待,一旦线程池中的有一个线程执行完任务,则去队列里面找有没有人在排队,如果有,按照先进先出的顺序获取一个任务去执行。
-
但是我们的队列也是有一定的容量的,加入当前线程池中工作线程达到10,等待队列也满了,我们再提交一个任务,我们会判断一个这个线程池是否还能再扩容,初始化线程池有个参数可以设置,如果可以扩容,我们创建一个新的工作线程,然后执行我们的新任务。
-
这时候就走图中的红色框 拒绝策略,也就是我们拒绝这一个任务,拒绝的方式有很多,什么事都不做,抛出异常,或者扩充线程池等等。
补充
别以为线程池的逻辑就这么简答,上述流程只是为了简单的说明线程池的工作流程。实际上线程池的具体业务逻辑实现非常的复杂,比如创建工作线程的方式,各种条件判断,等待队列,还有很多锁,CAS的东西,这里我就不再追加了!
线程池的创建方式
使用Executors方法(不推荐)
我们的Executors工具类提供了四个创建线程的方法,每种方法对应一种线程池,我们可以根据业务需求选择我们要是用的线程池,但是阿里巴巴编码规范明确表明不推荐使用这种现成的线程池。
我们以比较常用的定长线程池为例:
我们看到定长的线程池把最大长度和初始容量都定义为10,也就是定长,但是我们的等待队列LinkedBlockingQueue的长度就是Integer的最大值,也就是说如果我们的业务代码有问题,导致任务都卡在里面,其他的任务都在这里等,由于容量巨大,那么我们系统资源会迅速耗尽。
其他几种线程池都有类似把范围扩展到Integer.MAX_VALUE的迷惑操作。
ThreaPoolExecutor定制线程池
实际上,到了公司上班,我们一般自己定制线程池而不是是用Executors的方法创建,为什么呢?
很多人一直对线程池产生恐惧是因为参数比较多,源码复杂。其实我们是基本原理还是很简单的,如果停留在应用层面,创建一个类总不能难死你吧。
-
corePoolSize 核心线程池容量
-
maximumPoolSize 最大线程池容量
-
keepAliveTime 空闲线程存活时间
-
TimeUnit 空间线程存活时间单位
-
BolckingQueue
<Runnable>
工作队列 -
threadFactory 线程工厂(可选)
-
RejectedExecutionHandler 拒绝策略(可选)
我们结合图来分析:
-
corePoolSize 图中值为5,表示线程池可以有5个核心线程
-
BolckingQueue 工作队列,当我们第6个任务来了,而前5个任务还在执行,我们的新任务都去队列里等待
如果队列也满了,则对线程池进行扩容(创建一个工作线程),最大容量为 maximumPoolSize 。
-
如果 最大线程池容量也满了,根据你的 RejectedExecutionHandler 拒绝策略来选择如何处理这个新任务,报错还是什么事情都不干?
-
我们知道核心线程数是5,假如我们线程池中有空闲的线程,并且线程数大于核心线程数,我们把闲置的线程给清除掉,这也是我们线程池的优化策略。当然我们的优化策略是可以调整的,我们的参数 keepAliveTime 就表示一个线程闲置多久我们把它清除,TimeUnit 是我们的计时单位,有以下各种时间单位.
-
threadfactory是我们创建线程的工厂,比如把线程加工为守护线程,等等,一般我们不去添加这个参数。
线程池的基本操作
线程池有很多操作,查看当前工作线程数,等待队列的一些情况等等,对于我们而言,最最最基本的操作就是执行一个任务,我们用线程池的方式代替new Thread(new Runnable{…})
public class ThreadPoolDemo1 {
public static void main(String[] args) {
Executor executor=Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
//new Thread(ThreadPoolDemo1::run).start();
//代替Thread的方式创建线程
executor.execute(ThreadPoolDemo1::run);
}
}
public static void run(){
System.out.println(Thread.currentThread().getName());
}
}
定制线程池的基准
我们看到Executors自带的线程有非常严重的安全问题,那么我们应该如何定制线程池呢?
以一下结果参考因数:
- 电脑配置
- 并发级别
- 任务执行时间
-
如果电脑配置比较好,又追求极致的性能,我们的初始容量可以大一点。请更多的工人,付更多的工资。
电脑配置很差,核心线程数量调小一点,因为核心线程是内核线程,耗时相应增加。
-
一般而言,以电商为例。有点时候一秒的并发量达到百万甚至千万,但是每一个任务执行时间都非常的短暂。高峰期低峰期差异明显,如果我们把核心线程数加的很大,即便一万个线程也满足不了我们高峰期的爆发的任务量。所以我们应该把等待队列容量改的打一下,然后通过计算,选择合理的核心线程数,比如一天平均2亿订单,我们平均一秒就要处理2000多个订单,而我们的一个任务只需要0.2秒就执行完毕,那么我们可以设置500个核心线程,保证一天只能能处理完所有的订单
-
对于低并发,但是任务时长比较长的,举个例子,一个任务时10分,如果我们的核心线程过少,比如10个,那么我们执行100个任务平均下来算要花费100分钟,所以我们对于时间长的任务应该让他们尽量都被直接执行,相应的我们可以把等待队列缩小,缩小到0则没有等待队列,不够就加。