一、为什么要使用线程池
在使用线程池之前,我们得明白,为什么要用线程池?首先我们得了解,线程的调度是由操作系统来控制的,所以他就跟数据库连接池一样,采用了池设计。
线程是一个重量级的对象,同样应该避免频繁创建和销毁。
附赠一段线程启动的源码:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
上述代码是Thread类的线程启动方法,可以看到,start()方法里面最终调用的start0(),而start0()是一个native 方法,由此可见他调用底层操作系统的方法。也就是说,线程的启动是交给操作系统去完成的。
二、如何合理的创建线程池
既然知道了要使用线程池,那么如何创建合适的线程池呢?
首先创建线程池的核心是ThreadPoolExecutor。但是由于ThreadPoolExecutor的构造函数的参数太多太复杂,JAVA API就提供了一个简单的创建线程池的方法。就是Executors,例子如下:
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(()->{
System.out.println("hello world");
});
Executors能快速创建各种各样的线程池,但是并不建议使用Executors来创建线程池。先看下他的源码,
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到对于maximumPoolSize参数,他给的是Integer.MAX_VALUE。意思是在高负载的条件下线程数量可以无限的扩大。这就很容易引起OOM(Out of OutOfMemory)。
所以呢,咱们还是老老实实按实际情况通过ThreadPoolExecutor去创建线程池吧。下面附带ThreadPoolExecutor构造函数的参数简介,
1、corePoolSize:表示线程池保有的最小线程数。
2、maximumPoolSize:表示线程池创建的最大线程数。
3、keepAliveTime & unit:如果一个线程空闲了keepAliveTime &
unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
4、workQueue:工作队列,就是还需要执行的任务排队队列。
5、threadFactory:通过这个参数你可以自定义如何创建线程。
6、handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过
handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。
6.1、CallerRunsPolicy:提交任务的线程自己去执行该任务。
6.2、AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
6.3、DiscardPolicy:直接丢弃任务,没有任何异常抛出。
6.4、DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
三、线程数量创建多少个合适
其实对于线程池来说,最重要的还是线程个数。多了容易OOM,少了呢程序的吞吐量又上不去。那么我们如何来定这个线程的数量呢?
首先,我们要区分自己的程序是IO密集型还是计算密集型。
IO密集型
针对IO密集型,也是我们绝大多数的程序,他的最佳线程数
最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
计算密集型
既然是计算密集型,主要的压力就放在了CPU身上,那么他的最佳线程数理论上就是CPU的核数。但是在实际情况下,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。
最佳线程数 = CPU 核数 + 1
四、JAVA线程池工作原理
说起线程池的工作原理,又不得不说数据库连接池了。
虽然线程池和连接池都是池化设计,但是他们的工作原理却不一样。这从他们的使用过程中也能看出来。对于数据库连接池来说,他返回的是池内资源,也就是一个个的Connection。但是对于线程池来说,他却是把新任务直接加入连接池的工作列表里面去。参考下列例子:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
threadPoolExecutor.execute(()->{
System.out.println("hello world");
});
甚至说,JAVA在起名的时候,为了让大家清楚这并不是一个真正的“池“,他结尾也是以Executor(执行器)来命名的。
线程池的工作原理其实就是内部维护一个工作列表(workQueue ),execute方法把新任务加入到工作列表里面来,然后线程池内部的线程会取出工作列表的任务并执行。
五、Springboot自带的线程池
顺带提一下把,springboot在启动后,在单例池里面,已经有了一个连接池叫applicationTaskExecutor,当然线程池的实现还是我们熟悉的ThreadPoolExecutor,但是不建议直接用spring这个默认的线程池,原因是它跟Executors创建线程池的类似,线程池的最大数量也一样没有限制。