1.线程池是什么?
线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。
线程池的好处:一方面是避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
2.线程池详解
Java中的线程池核心实现类是ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
2.1 线程池参数详解:
- corePoolSize:核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了allowCoreThreadTimeOut。
- maximumPoolSize:最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。
- keepAliveTime:存活时间:如果经过 keepAliveTime时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。
- unit:keepAliveTime 的时间单位。
- workQueue:存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute方法提交的 Runnable 任务。
- threadFactory:(线程工程)用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。
- handler :(拒绝策略)当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。
2.2 线程池Demo:
BlockingQueue workQueue = new ArrayBlockingQueue(1024);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 1000, TimeUnit.MINUTES, workQueue);
for (int i = 0; i < 10; i++) {
int finalI = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + finalI);
}
});
}
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("aaaa");
}
});
第一个问题:ThreadPoolExecutor 的submit 方法和 execute方法有什么区别?
submit 本质跟execute没啥区别,但是submit的Runnable 使用Future 包装了,可以异步获取结果。
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
2.3线程池重要属性讲解
public class ThreadPoolExecutor extends AbstractExecutorService {
//ctl 能知道现在的工作状态和核心数量,前三位高水位区分线程池状态,后29为线程池的线程数量。最大不超过2^29-1
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
private final BlockingQueue<Runnable> workQueue;
private final ReentrantLock mainLock = new ReentrantLock();
private final HashSet<Worker> workers = new HashSet<Worker>();
private final Condition termination = mainLock.newCondition();
}
ctl是一个控制状态,对线程池的运行状态和线程池中的有效线程数量进行控制的一个字段。它包含两部分的信息:
- 线程池的运行状态(runState),高3位保存运行状态.相关方法是private static int runStateOf(int c){ return c& ~CAPACITY;}
~ 是按位取反 原本CAPACITY 是000111111… 而state是xxx00000 后面都是0,然后~CAPACITY 就变成了 11100000… 这样跟state进行与计算还是得到state
- 线程池内的有效线程数量(workerCount),ctl 是个Integer类型的数据,低29位保存;相关方法是 private static int workerCountOf(int c){ return c& CAPACITY;}
CAPACITY 是000111111… 而state是xxx00000 后面都是0,增加一个线程之后,state会变成xxx00001 ,这样State跟CAPACITY 进行与计算,得到1
- 控制状态的方法是:private static int ctlOf(int rs,int wc){return rs|wc;}
2.4 线程池的5种状态:
- RUNNING运行状态:
private static final int RUNNING = -1 << COUNT_BITS; 高3位111
- 状态说明: 线程池处于Running状态的时候,能够接收新的任务,并且对已添加的任务进行处理,任务不能大于规定的最大任务数;
- 状态切换: 线程池的初始状态是RUNNING,也就是说线程池一旦被创建就处于RUNNING状态,并且线程池中的任务数量为0;
- SHUTDOWN关闭状态:
private static final int SHUTDOWN = 0 << COUNT_BITS;高三位为000
- 状态说明: 线程池处于SHUTDOWN状态的时候,不接收新的任务,但是可以处理已经添加的任务;
- 状态切换: 调用线程池的shutDown()方法的时候,线程状态由RUNNING ---->>>>SHUTDOWN
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//检查关闭线程是否有权限
checkShutdownAccess();
//预先设置Shutdown状态
advanceRunState(SHUTDOWN);
//打断没有任务的线程,就是将阻塞在等待队列获取任务的线程打断出来,执行processWorkerExit 方法(适用所有线程包括核心)只留下正在跑任务的线程,此时有可能低于核心线程池数。
interruptIdleWorkers();
//钩子函数,自己实现,可以告知什么时候shutdown
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}//尝试进入TIDYING 最后进入结束状态
tryTerminate();
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
//为什么worker是AQS,打断之前需要判断worker有没有在运行,在运行就获取不到锁,防止正在运行的任务被打断掉。
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
- STOP 停止状态:
private static final int STOP = 1 << COUNT_BITS;高三位为001
- 状态说明: 线程池处于该状态的时候,不接收新的任务,不处理已接收的任务,并且还会中断正在处理的任务,中断并不代表线程被杀死了,并且清空阻塞队列
- 状态切换: 线程池调用shutDownNow()接口的时候,线程池由RUNNING(SHUTDOWN)-------->>>STOP
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//检查关闭线程是否有权限
checkShutdownAccess();
//预先设置STOP状态
advanceRunState(STOP);
//打断所有线程
interruptWorkers();
//返回等待队列里面还没有跑到的任务。
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
void interruptIfStarted() {
Thread t;
//此时不管有锁还是无锁,不管有没有跑任务,只要没被打断就进行打断。
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
- TIDYING整洁状态 :
private static final int TIDYING = 2 << COUNT_BITS;高三位为010
- 状态说明: 当所有的任务已经终止,ctl的值为0,线程池会变成TIDYING状态,当线程池处于该状态的时候会执行钩子函数terminated().terminated()方法在ThreadPoolExecutor中是空的,用户想在线程池变为TIDYING状态的时候处理东西,可以通过重载terminated()方法实现。
- 状态切换: 当线程池处于SHUTDOWN状态,并且阻塞队列中的任务为0,就会SHUTDOWN----->>>TIDYING,当线程池处于STOP状态下,线程池中执行任务数量为0,那么线程池状态STOP----->>>TIDYING.
- TERMINATED结束:
private static final int TERMINATED = 3 << COUNT_BITS;高三位为011
- 状态说明: 线程池已经彻底终止,就会变成TERMINATED状态
- 状态切换: 线程池处于TIDYING状态,执行完terminated()方法以后,就会实现TIDYING----->>>TERMINATED
进入该状态的条件如下:
1)线程池不是RUNNING状态;
2)线程池不是TIDYING状态或者TERMINATED状态;
3)如果线程池状态是SHUTDOWN,并且阻塞队列中的任务数量为0;
4)workerCount=0;
5)设置TIDYING状态成功;
线程池状态的切换如下图所示:
2.5 提交任务到线程池(如何防止并发的,没有锁关键字的)
- 未满核心线程池数量,就new新的线程,加入到线程池中 满了核心线程池数量。
- 优先用队列去接受,不要浪费线程资源。
- 实在不行,才考虑new一个新的线程去解决。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//获取当前线程池的线程数量 小于核心线程池数,并没有判断当前线程状态,addWorker里面会判断线程池状态
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//优先加入到队列中,减少新线程的启用,
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//二次判断是否运行,如果不在运行中,并且从队列中移除成功, 就进行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
//如果此时工作线程为0,就加一个工作线程进去。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//尝试采用新的线程,如果失败就拒绝任务
else if (!addWorker(command, false))
reject(command);
}
(1)提交任务和添加线程到线程池怎么防止并发的?
自旋+CAS ,增加线程池数量
ReentrantLock锁 将任务线程加到线程池集合中,然后运行线程。
- 判断线程池状态是否在运行,然后自旋+CAS,将线程池数量+1,加失败之后还要判断跟加线程之前的状态是否一致,不一致得重新获取线程状态,重来一次。
- 通过reentrantLock锁,new一个线程加入到线程池集合(HashSet)里面
- 如果new线程加入到线程池失败。会执行addWorkerFailed ,将线程池数量-1 的
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
//线程池运行状态
int rs = runStateOf(c);
// 当rs的值>=SHUTDOWN说明线程池不再接收新的任务了,然后判断以下三个条件:不会接受新的任务,有新的任务就返回。
//1、rs==SHUTDOWN 表示此时是关闭状态,不再接收新的任务,但是可以继续处理阻塞队列中的任务
//2、firstTask==null:firstTask为空
//3、!workQueue.isEmpty():阻塞队列不为空
//以上三个条件有一个不满足,则返回false,不创建新线程。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
//线程池线程数量
int wc = workerCountOf(c);
//判断是不是超了数量了(因为没有加锁,需要不断判断)
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//设置线程数量+1
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
//如果线程池状态改变了,需要重新进入线程池判断逻辑
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//new 一个新的Worker 线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//获取运行状态
int rs = runStateOf(ctl.get());
//此时是运行状态
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
//如果已经运行了就抛出异常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//线程池集合加入该线程
workers.add(w);
int s = workers.size();
//设置线程池数量的最大值
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//添加成功,就启动服务
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
(2)Worker线程运行方法
- firstTask 就是new Worker线程之后第一个要执行的任务
- getTask从队列中获取任务,需要考虑并发(多个线程从队列并发拿取任务,该如何解决)
- 执行任务
- 实在没任务了,执行processWorkerExit(w, completedAbruptly) 将这个线程移除
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // 刚启动线程,现将state 置为0,变成无锁状态
boolean completedAbruptly = true;
try {
//getTask 是从队列获取任务,有并发的可能。//如果超过一定的keepliveTime的时间没有获取到任务,就会被回收
while (task != null || (task = getTask()) != null) {
w.lock();
//如果线程正在停止,保证线程处于中断状态,如果不是的话保证当前线程不是中断状态;
//这里需要考虑执行If语句期间也执行了SHUTDOWNNOW方法,将状态直接置为STOP,
//同时还会中断线程池中的所有线程wt.interrupted()来判断是否中断,
//是为了确保在RUNNING和SHUTDOWN状态的时候是处于非中断状态。
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
//处于stop状态以上,需要把当前worker线程中断。
//线程池处于stop状态的时候,不接收新的任务,不处理已接收的任务,并且还会中断正在处理的任务,中断并不代表线程被杀死了,并且清空阻塞队列
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;
//worker 线程完成任务+1
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
//回收任务
processWorkerExit(w, completedAbruptly);
}
}
(3)如何并发从队列中获取任务getTask()
队列有AQS锁,有防并发,阻塞等待任务入队列唤醒的。
- allowCoreThreadTimeout 给核心线程数设置超时时间,决定了能不能减到核心线程数以下,减到最小1
- KeepAlive 决定的时候当线程数超过核心线程数,如果有限时间阻塞获取超时,就减少线程
- 如果没有超核心线程数,就无限阻塞等待,不需要减少线程。
//阻塞获取任务的。keeplive 获取任务
private Runnable getTask() {
//是否超时设置为false
boolean timedOut = false;
for (;;) {
int c = ctl.get();
//获取线程池的运行状态
int rs = runStateOf(c);
//如果状态是在关闭以上的状态 并且( 状态是停止以上或者队列为空),这个时候没有任务,也不该执行,应该减线程数。
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
//获取不到任务会进行线程数量减1
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
//有设置超时时间或者线程数大于核心线程数了
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//如果这个时候队列为空,而且超时获取不到的话
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
//获取不到任务会进行线程数量减1
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
//所以keeplive 是针对大于核心线程数的线程才会触发,如果小于核心线程不会触发。
try {
//但核心线程设置了超时时间(一般没有设置),或者线程数大于核心线程数,才会使用keepalive 获取任务。WorkeQueue实现了AQS能防并发。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
//不然都是阻塞到获取才行
workQueue.take();
if (r != null)
return r;
//只有走到这里的时候才会进行超时获取workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
(4)processWorkerExit(Worker w, boolean completedAbruptly) 处理线程
- 如果此时处于运行或者SHOTDOWN 状态,其实还是要执行任务的。
- 根据allowCoreThreadTimeOut是否允许核心线程数超时来判断,最少是1 还是 核心线程数
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly)
// 获取不到任务会进行线程数量减1.怕没有进行到getTask就直接processWorkerExit,兜底进行进行数量减1
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
//移除工作线程
workers.remove(w);
} finally {
mainLock.unlock();
}
tryTerminate();
int c = ctl.get();
//如果此时处于运行或者SHOTDOWN 状态,其实还是要执行任务的。
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return;
}
//因为可能核心数够了,任务丢到了队列里面,但是刚好核心线程池都获取不到任务,设置了允许回收核心线程导致没有线程跑任务了。
addWorker(null, false);
}
}
3.线程池思考题
- 添加任务到队列怎么防止并发 workQueue.offer(command)?
队列有AQS锁,有防并发
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
- 多个线程从队列并发拿取任务的时候
(队列有AQS锁,有防并发),阻塞等待任务入队列唤醒的。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
- 如何优雅停机,保证线程池的任务运行完
使用shutdown方法,能保证任务执行才停止
- 线程池状态停止了,线程池里面的任务该怎么办(优雅停机,销毁的时候调用shotdown()方法)
修改成shutdown 状态,是会等线程的任务和队列的任务执行完的,不会强行中断,
没有执行完不执行中断,等线程自己获取不到任务进行销毁回收。
没有运行,没有获取到锁的线程一般卡在了获取任务getTask 的workQueue.take()方法,被打断跳出阻塞等待,然后因为shutdown状态和队列任务为空,进行自我销毁。
//因为此时为shutdown状态,如果阻塞的话,其实说明队列没有任务了,就可以跳出去,自己销毁回收
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
- 如何回收线程
如果获取不到任务getTask()方法,就退出自选,移除线程池消除引用,调用processWorkerExit(w, completedAbruptly)方法