为什么使用线程池
在Java虚拟机的线程模型中,Java线程(Thread)被一对一映射为操作系统内核线程。Java线程启动时会创建一个操作系统内核线程;当Java线程终止时,操作系统内核线程也会被回收,这些操作会耗费资源。所以频繁的创建启动、销毁线程,会耗费操作系统较多资源。开发过程中,合理地使用线程池有2个好处:
1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2.提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
线程池实现原理
当向ThreadPoolExecutor执行execute()方法提交任务时,线程池处理流程如下:
图中线程池执行流程如下:
1)判断线程池线程数量是否已经达到核心线程数。如果否,则创建一个新的线程 来执行任务;如果是,则进入下个流程。
2)判断阻塞队列是否已满。如果否,则添加任务到阻塞队列,等待执行;如果是,则进入下个流程。
3)判断线程池线程数是否达到最大线程。如果否,创建一个线程来执行任务;如果是,则执行对应的拒绝策略。
ThreadPoolExecutor中线程执行任务流程如下:
图中线程池线程执行流程如下:
1)新创建一个线程时,会让这个线程执行当前任务。
2)这个线程执行完图中1任务后,会一直循环从阻塞队列BlockingQueue中获取任务来执行。当队列为空时,线程会被阻塞;当有新任务添加到阻塞队列时,阻塞的线程会被唤醒继续执行任务。
怎么使用线程池
线程池选择
在concurrent包下,Executors类中可以创建如下常见的线程池:
1.newCachedThreadPool:队列不存放任务,每次提交任务时,空闲线程不足都将创建新线程,线程空闲时间超过60秒将被回收。
2.newFixedThreadPool:创建固定线程数的线程池,使用的无界阻塞队列,可能存在OOM风险。
3.newSingleThreadExecutor:单线程线程池,可用于按顺序执行任务。
4.newScheduleThreadPool:可用于定时执行任务。
线程池创建
通常我们可以选择ThreadPoolExecutor手动指定参数来创建一个线程池:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1)corePoolSize:线程池核心线程数。当提交一个任务时,如果线程池线程数小于corePoolSize,则会新创建一个线程来执行任务。
2)maximumPoolSize:线程池最大线程数。
3)keepAliveTime:超过核心线程数小于最大线程数的线程,如果空闲时间超过这个值,将会被回收掉。
4)TimeUnit:keepAliveTime时间单位。
5)BlockingQueue:阻塞任务队列。
常用的几种阻塞队列: ArrayBlockingQueue(数组结构)、LinkedBlockingQueue(链表结构)、SynchronousQueue(不存储元素)、PriorityBlockingQueue(带优先级)。
6)ThreadFactory:创建线程的工厂。可以通过线程工厂给线程设置名称。
new ThreadFactoryBuilder().setNameFormat(“测试线程池-task-%d”).build()
7)RejectedExecutionHandler:当队列和线程池线程数都满了,执行拒绝策略。
常用线程池拒绝策略:
AbortPolicy:任务满时,直接抛出异常。
DiscardPolicy:任务满时,直接将任务丢弃,会造成数据丢失风险。
DiscardOldestPolicy:任务满时,将队列头任务丢弃,也会造成数据丢失风险。
CallerRunsPolicy:任务满时,由提交任务线程自己执行。
建议将线程池对象设置为静态变量或单例。以前在公司代码中有发现,系统每接收到一个请求,业务代码就创建一个线程池,且使用完后未shutdown(),线程会一直存活,这些线程对象和线程池对象不能被GC掉,时间一久系统很大可能出现OOM。
向线程池提交任务
execute()方法用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功。submit()方法会返回一个future类型结果,可以通过future的get()方法获取返回值,get()方法会阻塞当前线程直到任务完成。
线程池参数设置
任务类型分为:CPU密集型和IO密集型。CPU密集型任务压力主要在CPU上,所以配置的线程数不用太多,例如配置Ncpu + 1个线程;IO密集型任务压力主要在IO上,CPU比较空闲,所以配置的线程数需要多一些,例如2 * Ncpu。
任务队列尽量选择有界队列,有界队列能增加系统的稳定性和预警能力,无界队列系统可能会有OOM风险。
ThreadPoolExecutor线程池源码分析
execute()方法逻辑:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// ctl是一个32位原子变量,高3位表示线程池状态,低29位表示线程数量。
int c = ctl.get();
// 1.如果线程数量小于核心线程数,新创建线程执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果线程池状态为RUNNING,尝试将任务添加到阻塞队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3.如果2中添加任务到阻塞队列失败,尝试创建非核心线程执行任务
else if (!addWorker(command, false))
// 如果创建线程失败,执行拒绝策略
reject(command);
}
逻辑梳理:
1.thread会包装成Worker对象进行任务执行。
2.需要注意的是,ctl字段使用的是32位原子int字段,前3位表示线程池状态,后29位表示线程数量。通过对应位运算操作,可以获取到线程池的状态和线程数量,且每次线程数量和状态修改,都是对ctl的CAS操作,这样就保证了两个值始终是一致的。
addWorker(command, true)中代码太多,注意下面这两部分就行了:
compareAndIncrementWorkerCount(c)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
创建线程前,会通过CAS作将ctl加1。
下面这部分代码是将thread包装成Worker对象:
w = new Worker(firstTask);
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Worker(Runnable firstTask) {
setState(-1);
// 将runnable对象设置为first任务。
this.firstTask = firstTask;
// 获取线程池的线程工厂,传入当前worker对象创建线程
this.thread = getThreadFactory().newThread(this);
}
逻辑梳理:
1.将提交任务的runnable对象设置为first任务。
2.获取线程池线程工厂,创建线程。Worker实现了Runnable接口,这里创建线程时传入的是worker对象,不是提交任务时的runnable对象,所以,调用thread.strat()时,是执行worker对象的run()方法。
执行thread.strat()方法,会调用worker对象run()方法,run()方法会调用runWorker(Worker w)方法,源码逻辑:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
// 注意这行代码,第一个任务执行完后,会循环从阻塞队列中获取任务执行
// 当从队列中获取任务为空时,线程会被阻塞;当有新任务添加到阻塞队列
// 后,阻塞的线程会依次被唤醒继续执行任务
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
关闭线程池,shutdown()方法逻辑:
public void shutdown() {
// 1.获取到线程池全局锁,保证只能同时有一处关闭操作
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 2.权限校验
checkShutdownAccess();
// 3.CAS操作将ctl高3位设置为SHUTDOWN
advanceRunState(SHUTDOWN);
// 4.中断线程池线程
interruptIdleWorkers();
onShutdown();
} finally {
mainLock.unlock();
}
tryTerminate();
}