线程池是生产者消费者模式,它解决的问题是协调生产者和消费者的速率,同时避免线程切换,提升运行速度。
下面是我对线程池的一些理解,主要原理图如下:
一、java线程池原理
参考如下文章
核心一:死循环获取任务执行
初始化一些线程,它们一直执行一个while死循环方法,在该方法中,不停地去blockingqueue中获取任务,然后执行,如下:
private void runWorker(Worker worker) {
if(worker==null) throw new NullPointerException("空的工作线程");
try {
Runnable firstTask = worker.firstTask;
Thread wk = worker.thread;
worker.firstTask = null;
// 当前的任务先给他完成了
while(firstTask!=null || (firstTask=getTask())!=null){
// 如果执行线程池的stop方法,所有工作队里都要进行interrupted
if(wk.isInterrupted()){
System.out.println("this thread is interrupted");
}
if(status.get()==STOP){
System.out.println("this threadPoll has already stopped");
}
firstTask.run();
firstTask = null;
worker.finishTaskCount++;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
// 没任务了退出
while (true){
// 原子类的操作有可能失败,所以要不停重试,直到成功为止
if(casDeleteWorkerCount()){
finishedTaskCount += worker.finishTaskCount;
break;
}else{
continue;
}
}
L.lock();
try {
workers.remove(worker);
} finally {
L.unlock();
}
}
}
核心二:阻塞队列协调生成者消费者速率
如果一直死循环下去,对cpu是比较大的浪费,blockingqueue可以让线程在没有任务时,阻塞停止获取任务,当有任务时再唤醒线程。同时插入任务时,队列满了也能执行拒绝策略,阻断生成者。
二、blockingqueue原理
线程池主要核心是阻塞队列,下面再看下阻塞队列的原理:
blockingqueue是通过加Reentrantlock锁的方式实现的添加元素时,添加元素时,先获取锁,然后判断队列是否为空,如果为空调用condition.wait()方法,将线程阻塞,如下:
/**
* 从队列中取一个元素
* @return [description]
* @throws InterruptedException [description]
*/
public E take() throws InterruptedException {
//对操作进行加锁 多线程时轮流取元素
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果队列中没有对象 则阻塞线程等待
while (count == 0)
//重点:等待存数据的线程通知
notEmpty.await();
//代码运行到此处说明count!=null 执行从队列中取元素
return dequeue();
} finally {
lock.unlock();
}
}
而condition.wait()是aqs中的方法,它将当前线程封装成一个节点,插入到aqs中,下面再看下aqs原理。
三、aqs原理
aqs是抽象队列同步器,顾名思义,它是用来进行线程间同步的,也就是加锁,然后它是使用一个state变量和一个队列来实现的。获取锁时,先使用cas来设置state变量,设置成功代表获取到了锁,设置失败则将当前线程包装成队列的一个节点,插入队列中,并且使用LockSupport.park方法阻塞当前线程。如下:
/**
* 让线程不间断地获取锁,若线程对应的节点不是头节点的下一个节点,则会进入等待状态
* @param node the node
*/
final boolean acquireQueued(final Node node, int arg) {
// 记录失败标志
boolean failed = true;
try {
// 记录中断标志,初始为true
boolean interrupted = false;
// 循环执行,因为线程在被唤醒后,可能再次获取锁失败,需要重写进入等待
for (;;) {
// 获取当前线程节点的前一个节点
final Node p = node.predecessor();
// 若前一个节点是头节点,则tryAcquire尝试获取锁,若获取成功,则执行if中的代码
if (p == head && tryAcquire(arg)) {
// 将当前节点设置为头节点
setHead(node);
// 将原来的头节点移出同步队列
p.next = null; // help GC
// 失败标志置为false
failed = false;
// 返回中断标志,acquire方法可以根据返回的中断标志,判断当前线程是否被中断
return interrupted;
}
// shouldParkAfterFailedAcquire方法判断当前线程是否能够进入等待状态,
// 若当前线程的节点不是头节点的下一个节点,则需要进入等待状态,
// 在此方法内部,当前线程会找到它的前驱节点中,第一个还在正常等待或执行的节点,
// 让其作为自己的直接前驱,然后在需要时将自己唤醒(因为其中有些线程可能被中断),
// 若找到,则返回true,表示自己可以进入等待状态了;
// 则继续调用parkAndCheckInterrupt方法,当前线程在这个方法中等待,
// 直到被其他线程唤醒,或者被中断后返回,返回时将返回一个boolean值,
// 表示这个线程是否被中断,若为true,则将执行下面一行代码,将中断标志置为true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 上面代码中只有一个return语句,且return的前一句就是failed = false;
// 所以只有当异常发生时,failed才会保持true的状态运行到此处;
// 异常可能是线程被中断,也可能是其他方法中的异常,
// 比如我们自己实现的tryAcquire方法
// 此时将取消线程获取锁的动作,将它从同步队列中移除
if (failed)
cancelAcquire(node);
}
}
其中cas是比较并替换,由cpu包装这条指令执行的线程安全性。LockSupport.park方法底层是c语言写的方法。
四、线程阻塞
aqs主要是可以阻塞当前线程,那线程阻塞是什么意思呢?
线程和进程其实差不多,linux内核中把线程也当做进程来处理。学习c语言代码时,首先会写个main函数,代表一个程序的进程,所以线程其实就是一段函数,它存储在内存中,主要包含线程的堆栈,代码,当前执行到哪一行和一些数据,还有描述线程的一些状态信息等等。线程的阻塞其实就是改变当前线程的状态,使得cpu不执行当前线程。LockSupport.park是将当前线程的状态变成等待。
五、同类框架
线程池是使用加锁+线程阻塞的方式解决生产者消费者问题的,那有其他方式吗?lmax公司开发了一款disruptor框架,采用无锁的方式来实现,效率比线程池高很多。
disruptor是使用一个环形队列来存储任务,生产者存的时候,首先使用cas获取队列中可以写的位置,然后写队列,这里就是无锁的操作。消费者读队列的时候,首先查询available queue中的位置,获取可以读的位置,然后进行读操作。
消费者没有消息时的等待策略:
BlockingWaitStrategy(阻塞等待策略)SleepingWaitStrategy(休眠等待策略)YieldingWaitStrategy(让出等待策略)BusySpinWaitStrategy(忙等待策略)
具体实例参考:并发框架——Distruptor_disruptor-CSDN博客
六、实际应用
1、web服务器接收请求时,使用线程池,可以避免线程创建过多,拖垮服务器
2、消费mq消息时,使用线程池可以加快消息的消费
3、日志打印时,使用线程池可以异步打印,提高接口性能
4、excel文件导入时,可以使用线程池异步导入,返回给用户导入中的状态,前端再不停地请求状态
5、接口中包含多个第三方接口请求,可以用线程池并行执行,减少用户等待
6、执行大量耗时的操作,比如文件下载,可以使用线程池分段并行下载
7、连接池也是使用线程池,每个线程保存连接,减少连接创建的时间
......
七、线程池使用问题
1、threadLocal存储用户信息,在线程池中不能传递,导致获取失败
解决方法:使用TransimittableThreadLocal,GitHub - alibaba/transmittable-thread-local: 📌 a missing Java std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.
2、主线程和子线程使用同一线程池执行,造成线程池死锁
解决方法:不同业务分开定义线程池
3、生产者的速率比消费者快,导致线程池满,执行拒绝策略
解决方法:可以使用SynchronousQueue,让调用者自己执行
4、线程池内抛出异常没有try catch
如果是调用execute方法执行任务,抛出异常到外层,当前线程结束,添加一个新的工作线程
如果是调用submit方法执行任务,不抛异常,调用get方法获取结果时才抛异常,当前线程变成wait状态
如果是ScheduledThreadPoolExecutor,抛出异常,定时任务终止
解决方法:线程池内的任务需要加try catch
5、线程池核心线程数量设置
cpu密集型cpu核数+1,io密集型cpu核数*2+1,这里理论值,实际生产中还需要通过接口压测的方式来确定线程数量