浅谈JAVA线程池实现原理及与一般池化技术的区别
1.为什么要使用线程池
我们经常使用线程池,那为什么要用线程池呢?它解决了什么问题呢?有的同学说,这还不简单,因为频繁手动创建线程会造成的开销大。但是这样的回答显示是太过于笼统了。
JAVA创建一个对象,只需要在JVM堆上分配一块内存,但是要创建一个线程,则需要调用系统内核的API,然后操作系统为线程分配一系列的资源,这样的成本开销就很大了。我们创建一个线程,JVM虚拟机主要做一下操作:
- 它为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧;
- 每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成
- 每个线程获得一个程序计数器(PC),告诉它当前处理器执行的指令是什么;
- 系统创建一个与Java线程对应的本地线程;
- 将与线程相关的描述符添加到JVM内部数据结构中
- 线程共享堆和方法区域
创建一个线程,大约需要1M作用的内存空间。所以,如果在高并发下,如果我们无限制的创建线程,很容易造成OOM;
除此之外,CPU中执行上下文的切换,导致CPU中的「指令流水线(Instruction Pipeline)」的中断和CPU缓存的失效。如果线程太多,线程切换的时间会比线程执行的时间要长,严重浪费了CPU资源。同时对于共享资源的竞争(锁)会导致线程切换开销急剧增加。
小结:
1、额外的占用,产生OOM;
2、频繁的线程切换,造成CPU资源的浪费;
3、频繁的线程切换,造成共享资源竞争加剧。
所以,线程是一个重量级的对象,应该避免频繁创建和销毁。因此,我们要用池化技术,来减少手动创建过多线程对系统性能的影响。
2.JAVA线程池与一般池化技术的本质区别
既然说到线程池,那我们先说一下池的概念。池(Pool)技术在一定程度上可以明显优化服务器应用程序的性能,提高程序执行效率和降低系统资源开销,比如数据库连接池、线程池、内存池、对象池等。我们以数据库连接池为例来说说明一下。
数据库连接是一种关键的有限的昂贵的资源,为了解决资源的频繁分配﹑释放所造成的问题,我们为为数据库连接建立一个“缓冲池”,在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后释放连接,再放回去。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。所以,数据库连接池的基本使用方法就是acquire资源,使用完成后,release资源。
class DBConnectionPool{
// 获取池化资源
DBConnection acquire() {
}
// 释放池化资源
void release(DBConnection connection){
}
}
但线程池却不是这种模式,这也是我最初接触线程池感觉比较难理解的地方。因为查阅线程池相关的API,里面压根就没有申请线程和释放线程的方法。
其实线程池更像一种生产者-消费者模式。那为什么采用这种模式呢?
我们假设创建一个线程池,里面有10个线程,当我们使用线程时,创建一个线程对象:
T t1= new Thread();
然后从线程池中获取一个线程:
Thread poolThread = ThreadPool.acquire();
然后我们用poolThread 来执行t1:
即:
poolThread.execute(t1)。
但是很可惜,我找遍Thread中的API,并没有找到这样的方法,所以线程池无法使用一般的池化方法来处理。
下面是伪代码:
class ThreadPool{
Thread acquire() {
}
void release(Thread t){
}
public static void main(String[] args) {
ThreadPool pool = new ThreadPool();
Thread poolThread =pool.acquire();
poolThread.execute(t1)。
}
}
3.JAVA线程池基本实现原理简介
从上面的分析我们知道,JAVA线程池是采用生产者-消费者模式来实现的,那么生产者和消费者分别是谁呢?其实线程池的使用者就是生产者,线程池本身就是消费者。
1.手写线程池
为了更好的说明这个问题,我写了一个简单的线程池(仅限于我们理解相关内容),代码如下:
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class SimpleThreadPool {
/**
* 阻塞队列实现生产-消费
*/
private BlockingDeque<Runnable> workQueue;
/**
* 存储线程池的内部工作线程
*/
private Set<Worker> workers = new HashSet<Worker>();
/**
* 构造方法
*
* @param poolSize
* @param workQueue
*/
public SimpleThreadPool(int poolSize, BlockingDeque<Runnable> workQueue) {
this.workQueue = workQueue;
for (int i = 0; i < poolSize; i++) {
//实例化线程池内部工作线程
Worker worker = new Worker();
//启动线程池内部工作线程,并加入的workers
worker.start();
workers.add(worker);
}
}
/**
* 提交任务
*
* @param command
*/
public void execute(Runnable command) throws Exception {
workQueue.put(command);
}
/**
* 线程池内部工作线程,负责消费队列中的task,并执行task
*/
class Worker extends Thread {
@Override
public void run() {
//维护一个死循环,循环消费并执行task,这里是一个阻塞的
for (; ; ) {
try {
Runnable task = workQueue.take();
task.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
//创建有界队列
BlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<Runnable>(2);
//创建线程池
SimpleThreadPool simpleThreadPool = new SimpleThreadPool(2, workQueue);
simpleThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("Hello world!");
}
});
}
}
在 SimpleThreadPool 的中,我定义了一个阻塞队列 workQueue 和一组工作线程works,工作线程的个数由 poolSize 来指定。在测试汇中,我通过调用 execute() 方法来提交 Runnable 任务,execute() 方法的内部实现仅仅是将任务加入到 workQueue 中。SimpleThreadPool 内部的工作线程会消费 workQueue 中的任务并执行任务,就是Worker 中的那段死循环代码。这就是线程池的基本工作原理。
2.JAVA中的线程池
Java 提供的线程池相关的工具类中,最核心的是 ThreadPoolExecutor,它的类继承关系如下:
从上面我们可以看出,它定义的这个线程池,更强调的是Executor,即执行器。它的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:核心线程数,即线程池中至少要保持的工作线程数;
maximumPoolSize:最大线程数,即线程池中最大能够运行的工作线程数;
keepAliveTime +unit:如果一个线程空闲了keepAliveTime & unit这么长时间,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了;
workQueue:线程池的工作队列;
threadFactory:通过这个参数可以自定义如何创建线程,例如线程的名字;
handler:自定义任务的拒绝策略。如果线程池无法再提交新的任务,线程池就会拒绝接接收
ThreadPoolExecutor 已经提供了以下 4 种策略:
- CallerRunsPolicy:提交任务的线程自己去执行该任务。
- AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
- DiscardPolicy:直接丢弃任务,没有任何异常抛出。
- DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
3.注意事项
1.如何正确的创建线程池
在阿里巴巴开发手册中,有这么一条规范,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式能够更加明确线程池的运行规则,规避资源耗尽的风险:
1.newFixedThreadPool和newSingleThreadScheduledExecutor:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
2.newCachedThreadPool和ScheduledExecutorService:允许的创建线程数量为Integer_MAX_VALUE,可能会创建大量的线程,从而导致OOM.
2.如何正确理解corePoolSize和maximumPoolSize
在学些线程池的时候,corePoolSize和maximumPoolSize曾经困惑我我很长时间,不知道大家有没有遇到麻烦,现在这里把这两个参数重申一下:
corePoolSize,代表的是线程池的核心线程数,maximumPoolSize代表的是线程池中的最大可运行的线程数,假如一个线程池,corePoolSize=2,maximumPoolSize=10,workQueue的size为5,如果此时同时请求10个线程,那么当前线程池中共有多少个线程在运行?我之前的答案是10个,这显然是错误的,正确答案应该是5个(2个核心线程+3个最大线程),剩余的5个在workQueue的队列中。即队列execute的顺序是:核心线程数 ——》workQueue——》最大线程数。
到此,关于JAVA线程池的一些基本原理就描述完了,下一篇博客我将从源码角度来分析JAVA线程池的实现。