什么是线程池
线程池是为了解决高并发多线程下面频繁创建线程,销毁线程,带来大量的线程调度的资源消耗问题的,也就是说有了线程池,来了一个任务,就不需要我们手动创建线程,而是将任务交给线程池去处理,这样就可以节省了大量的系统资源。
- 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 --阿里巴巴开发手册
创建一个线程池
在java中有五个线程池的实现,
可以看到线程池ThreadPoolExecutor有四个构造方法,newScheduledThreadPool、newWorkStealingPool、newFixedThreadPool、、newCachedThreadPool、newCachedThreadPool。
package pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService fixedFhreadpool = Executors.newFixedThreadPool(5);
ExecutorService singleThreadpool = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadpool = Executors.newCachedThreadPool();
System.out.println("newFixedThreadPool===================");
try {
for (int i = 0; i < 10; i++) {
fixedFhreadpool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
fixedFhreadpool.shutdown();
}
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("singleThreadpool===================");
try {
for (int i = 0; i < 10; i++) {
singleThreadpool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
singleThreadpool.shutdown();
}
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("cachedThreadpool===================");
try {
for (int i = 0; i < 10; i++) {
cachedThreadpool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
cachedThreadpool.shutdown();
}
}
}
上面是代码以及运行结果,这边演示了三种线程池的操作,其余两个newScheduledThreadPool是带任务调度的的线程池,newWorkStealingPool下面再讲。
跟踪到这三个方法的中。
可以看到这三个方法用的是同一个构造方法,这里传入了五个参数,继续跟进这个ThreadPoolExecutor构造方法,
继续跟进
最终发现万变不离其宗,所有的线程池都是通过这个方法来构造的。
- corePoolSize //线程池初始线程数 银行默认开的窗口数
- maximumPoolSize //线程池最大线程数 银行最大窗口数
- keepAliveTime //线程关闭时间 银行临时窗口的等待关闭时间
- unit //时间单位
- workQueue //存放任务的队列 顾客的等待区
- threadFactory //线程工厂,一般用默认
- handler //拒绝策略 最大窗口满了,等待区满了,所采取的策略
怎么去理解这7个参数,这里引用阳哥的银行例子来举例。
银行现在默认有2个窗口处理业务,还有3个临时窗口不忙的时候是关闭的,
①正常处理业务2个窗口够了,
②后来的人就在等待区等待,·
③但是遇到高峰期,就需要紧急加3个窗口来处理业务,这个时候如果worker达到了最大,并且等待区也满了,并且还有外来顾客,
④那么就会启动拒绝策略,DiscardOldestPolicy-把队列头的任务(等待最久的)去掉,AbortPolicy-抛异常,CallerRunsPolicy-让发送任务的线程来执行任务,DiscardPolicy-什么都不做,无作为。
⑤接下来如果高峰期过了之后,3个紧急窗口超过一定的时间没有处理业务,就会关闭窗口。
对应的线程池的执行步骤。
再啰嗦一遍,这边是阳哥的资料。
newFixedThreadPool
这个时候再回到newFixedThreadPool,传入了一个固定线程数然后将初始化和最大线程数设置为nThreads,就是固定线程数,就是fixed,当然也就不需要等待时间,然后传入一个堵塞队列LinkedBlockingQueue,
因为是固定的,所以不需要打开关闭线程,性能很稳定,适用于执行长期任务。
newSingleThreadExecutor
线程池的初始大小为1,最大也为1,就是一次只能执行一个任务,也就是Single
适用于一个任务一个任务执行的的场景。
newCachedThreadPool
从名字来看是可缓存的,他的初始化是0个,最大值是int的最大值,也就相当于无限大
适用于执行很多短期的异步的小程序或加载较轻的服务
这几个都不被推荐使用,所以只能自定义了。
package pool;
import java.util.concurrent.*;
public class MyThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
try {
for (int i = 1; i <= 12; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务"+ finalI);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}
}
方法很简单使用这个就可以了ThreadPoolExecutor,这里定义了初始化为2,最大为5,等待时间2s,堵塞队列大小为5,拒绝策略DiscardOldestPolicy的一个线程池,可以看到执行之后任务3,4没有执行到,也就是说这两个任务被拒绝策略去掉了,
所以需要根据具体业务来设置这个值,也需要根据硬件情况来设置参数,一般如果是 CPU密集型的(需要大量的运算,CPU的使用密度很高),最大线程数一般设置为CPU核数+1,Runtime.getRuntime().availableProcessors()得到核数,如果是IO密集型(读取写入的操作比较多,传输比较多,读取数据库之类的)配置为CPU核数*2,CPU核数/(1-堵塞系数)
原理(从源码角度解释线程池工作过程)
先解释一下这里的几个参数(这里最好自己打开源码结合源码看参数,我只截图了主方法)
参数萌
- commond是任务
- ctl是一个原子整型(原子整型),ctl的高三位isRunning( c)表示线程池的五中状态,低28位workerCountOf( c)表示线程池中开启的线程的个数,get()方法就是拿到里面的int值
- core 是否是核心线程
- workerStarted
- workerAdded
- workers 是一个hashset里面存放worker
execute
这里几个判断
- 如果commond是空,抛异常
- workerCountOf( c)拿到当前启动的线程个数,如果小于corePoolSize核心线程数,就执行
addWorker(command, true)
(这个方法很重要下面重点讲)添加worker,并且把command放到添加的worker里面执行 - workQueue.offer(command)是将任务放入队列,没什么好讲的,返回是否成功,这边添加完成之后本来就可以完事了,但是接下来又做了一个验证,保证线程池正在运行。下面如果线程数是0,就执行
addWorker(null, false)
,为什么传空呢?很重要!!!! - 如果添加失败,拒绝策略处理任务
addWorker
addWorker
addWorker
retry:就是goto的意思,
line903
进入for循环,line908
判断是否再运行,检查队列是否是空line914
接下来for循环,line916
判断如果开启线程数大于28位最大值,如果大于线程最大数,line918
就不能创建,双重判断,接下来,line919
compareAndIncrementWorkerCount( c)写时复制,将线程数+1添加成功,line920
跳出两个for,line922
失败line923
CAS自旋再添加- 接下来就是添加线程阶段,
line932
新建一个worker,line933
拿到线程,注意这里用的是final,line934
如果t不是空,line936
上锁,line943
判断线程池的工作状态,并且确认firstTask不为空,line945
然后再次检查t是否启动,再t启动之后把,line947
worker添加到workers里面,line949
再次检查worker的数量是否大于最大数量,如果大于就将最大数量设置为workers大小,line951
标记workerAdded为true,添加成功,并且这边是把任务也放进了worker里面执行了的,至此addworker就完成了。
所以addWorker(command, true),这个逻辑就是正常添加worker,而addWorker(null, false)就是添加一个不带任务的woker
至此,上面的代码就完成了,初始化worker,并且创建线程把worker放入执行,来了任务将任务放入队列,以及队列满了之后的拒绝任务,接下来还需要找到,从队列中取出任务来执行,,这段逻辑
runWorker
这个方法从line1134
开始看,task = getTask()是在while循环里面的,每次都从队列里面取出任务,里呢146
beforeExecute(wt, task)是预留的一个空方法,执行之前的操作,line1149
可以看到真正的执行任务的代码。也就是说某个woreker一但run起来,就会从队列中取出任务来执行,
getTask
还有一个逻辑,线程超时会自动回收,这段逻辑再getTask方法中,看到先进行一些判断,然后line1062
开启线程数大于核心线程数的情况下为true,line1066
自旋任务数减1,line1072
timed是true,line1073
会从队列中拉取一个任务并且设置当前线程的超时时间,如果时间默认为60s,如果超时那就结束当前线程,
至此,整个线程池的大概流程到这里就结束了。
总结
看完整个源码,感慨万分,jdk源码的设计中有太多太多可以学习的了,在使用对象前,一定要进行非空检查,这是一个习惯,这里初略的写完了线程池的 整个流程,以及实现原理,七大参数,当然线程池不止这些,还有很多是需要继续看源码学习的,看源码不仅仅是学习逻辑,设计思想,还有代码风格和编程习惯也可以学习学习,共勉。