==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍
摘要:上篇实现了简单的无锁线程池,中篇实现了简单的加锁线程池,本篇着重剖析java线程池源码。
一、基础概念
其实,把下面这些参数概念理解了,看线程池源码像看helloworld一样简单。
- corePoolSize
核心线程数量,什么是核心线程呢?就是一创建出来就常驻内存,不退出不销毁,直观点看,代码长这样public void run(){ while(true){ //不停的执行任务或者等待执行任务 } }
- maximumPoolSize
最大线程数量,什么是最大线程数呢?就是这个线程池最多能同时运行的线程数量。比如核心线程数是5,最大线程数是10,那么当5个核心线程不够用的时候,会最多再创建5个线程,达到最大线程数10。那么疑问就来了,非核心线程会被回收吗?答案是不确定,根据你设置的存活时间来定。 - keepAliveTime
线程保持存活的时间,通俗的讲就是,某个线程在给定的时间内没有接活,那么就会退出销毁了。那么这个设置只对非核心线程有效吗?答案是否定的,线程池还有个参数可以设置核心线程也能被回收。 - BlockingQueue
阻塞队列,什么是阻塞队列呢?和普通的队列有什么不一样?这人家的名字不是已经告诉你了嘛,区别在于人家可以阻塞。就是当队列为空的时候可以阻塞在那里等着。来简单看看其中一个阻塞队列LinkedBlockingDeque
的实现。public E takeFirst() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lock(); try { E x; //如果队列一直为空,那么就一直阻塞在那 //当队列不为空了,notEmpty.await()会被唤醒 while ( (x = unlinkFirst()) == null) notEmpty.await(); return x; } finally { lock.unlock(); } } …… public E pollFirst(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { E x; //这个和take不一样的是,可以指定等待时间,超时直接返回null while ( (x = unlinkFirst()) == null) { if (nanos <= 0) return null; nanos = notEmpty.awaitNanos(nanos); } return x; } finally { lock.unlock(); } }
- ThreadFactory
线程工厂,听着名字像是生产线程的,没错,就是字面意思。这是线程池将线程的创建开放出来给用户自定义,可以看个简单的例子。
通过线程工厂,我们能对线程进行个性化的定制,再具体的就不演示了。new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, "我是从线程工厂出生的"); } };
- RejectedExecutionHandler
拒绝执行策略,通俗点就是,当任务执行不过来了,你可以自定义一个处理策略,默认的策略就是直接抛异常。但是在实际应用中,应该自定义一个比较友好的处理策略。 - ctl
是不是差点没认出来,这就是java线程池里到处出现的一个参数。
可以看一下源码中的注释是如何解释这个缩写的参数的,它其实就是一个控制线程池大小和状态的参数。private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
我们将它定义的状态给打印出来看看,就能一目了然。int COUNT_BITS = Integer.SIZE - 3; int CAPACITY = (1 << COUNT_BITS) - 1; int RUNNING = -1 << COUNT_BITS; int SHUTDOWN = 0 << COUNT_BITS; int STOP = 1 << COUNT_BITS; int TIDYING = 2 << COUNT_BITS; int TERMINATED = 3 << COUNT_BITS; System.out.println("COUNT_BITS = " + COUNT_BITS); System.out.println("CAPACITY = " + CAPACITY + " = 000" + Integer.toBinaryString(CAPACITY)); System.out.println("~CAPACITY = " + ~CAPACITY + " = " + Integer.toBinaryString(~CAPACITY)); System.out.println("RUNNING = " + RUNNING + " = " + Integer.toBinaryString(RUNNING)); System.out.println("SHUTDOWN = " + SHUTDOWN + " = " + Integer.toBinaryString(SHUTDOWN)); System.out.println("STOP = " + STOP + " = 00" + Integer.toBinaryString(STOP)); System.out.println("TIDYING = " + TIDYING + " = 0" + Integer.toBinaryString(TIDYING)); System.out.println("TERMINATED = " + TERMINATED + " = 0" + Integer.toBinaryString(TERMINATED));
可以看到,一个32位整型,容量用29位来表示,剩下的3位表示状态。只要ctl<0
那么线程池就是正常运行的,如果ctl>=0
就是要关闭了。 - 自旋
通俗点就是死循环for(;;);
,可能有人会问,为什么没见过用while
来自旋的?其实吧,都行,从现在来看,两者效率基本一样。 - CAS
CAS全称是compareAndSet,就是比较并更新的意思,这是一个原子操作,由底层汇编指令实现。使用到的汇编指令是cmpxchg
,其作用是比较并交换操作数(感兴趣的百度一下这个指令的具体释义)。java是不能直接使用汇编的,那怎么办呢?java可以通过C语言间接的去使用这个指令,那么就引出了Unsafe
这个类,简单的讲,这个类给你提供了类似C/C++的操作内存的方法,所以非常危险,我们如果要使用只能通过反射拿到它进行使用。 - AQS
AQS全称是AbstractQueuedSynchronizer
,它是一个队列同步器。它的作用就像是一条管道一样,严格的控制线程的先进先出,任一时刻,有且仅有一个线程能运行,其它都会被阻塞。比如说,有2个线程,同时争夺一个资源,那么线程1先拿到了,线程2再想去拿,就拿不到,并且放到队尾排队,只有线程1释放了这个资源,出队了,线程2才能拿到。依次类推。关于AQS后续通过其它文章再深入探讨。
二、ThreadPoolExecutor源码剖析
通过上面对基础概念的解释,这里就不再重复对其构造函数分析了。
1.任务提交的三种方式
void execute(Runnable command)
这种方式提交的任务,是没有返回值,也没有Future异步等待task执行结束。Future<?> submit(Runnable task)
这种方式提交的任务,也没有返回值,有Future异步等待task执行结束。
这里对task的包装挺巧妙的,因为接口可以多继承,所以包装一个新的接口public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; } …… protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { return new FutureTask<T>(runnable, value); } …… public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
RunnableFuture
继承自Runnable
和Future
,这样再通过中间人FutureTask
将Runnable
和Callable
包装起来,完美!<T> Future<T> submit(Callable<T> task)
这种方式提交的任务,必须有返回值(也就是T不能为Void),有Future异步等待task执行结束。
这三种提交方式,最后都由execute
执行。
2.线程池入口execute源码剖析
- 代码分析
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); //判断当前线程池的大小是否小于核心线程数 if (workerCountOf(c) < corePoolSize) { //如果还未达到核心线程数,那么就继续创建线程 if (addWorker(command, true)) return; //这里尝试获取最新的线程数量 c = ctl.get(); } //如果线程池大小已经达到核心线程数,那么就放到队列中排队 if (isRunning(c) && workQueue.offer(command)) { //不管是多少次检查,都是为了尽可能感知并发下的状态变化 int recheck = ctl.get(); //如果线程池关闭了,那么就不要再往队列里面加任务了,这里 //尝试删除之前加入的那个任务,注意,这个操作是可能失败返回 //false的,因为你一加进去,可能就立马被执行了。如果删除成功 //就使用相应的拒绝策略处理 if (! isRunning(recheck) && remove(command)) reject(command); //这个就比较耐人寻味,线程池正在运行并且大小为0的时候创建一个线程 else if (workerCountOf(recheck) == 0) addWorker(null, false); } //这个就很好理解了,当核心线程满了,队列满了,就创建非核心线程 //来处理,如果非核心线程也满了,就执行相应的拒绝策略处理了 else if (!addWorker(command, false)) reject(command); }
- Q&A
1) 第一个if已经判断当前线程池大小没有达到核心线程数了,为什么addWorker
还可能返回false?
这个问题在我没有自己手撸一个线程池的时候,我看了好多遍也没理解。我们来假设这么一个场景,核心线程数为2,最大线程数为4,多线程并发提交任务,是不是有可能会发生同时进行int c = ctl.get();
或者前后相隔非常非常短的时候进行int c = ctl.get();
,这导致的结果是什么呢?线程没创建出来,线程池大小没更新,大家拿到的都是一个值,都小于corePoolSize
,所以需要在addWorker
里面进行拦截判断(具体的下面再讲),如果已经有2个核心线程创建完了,那么剩下的2个就会返回false,继续进行下面的分支判断。
2)在什么情况下这个workerCountOf(recheck) == 0
为true
?
为什么线程池运行的时候,并且经过前面核心线程的创建,还会出现线程数为0的情况。这个我也还没确切的结论。
3.创建工作线程addWorker源码剖析
- 代码分析
private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); //拿到线程池运行状态 int rs = runStateOf(c); //1.线程池状态大于等于SHUTDOWN //2.!(线程池状态正好等于SHUTDOWN && firstTask == null && ! workQueue.isEmpty()) //同时满足第一个条件和第二个条件,就不用继续往下创建工作线程了 //通俗的解释这段代码的意思,第一个条件很容易明白,就是线程池要 //关闭了,不能创建工作线程了。第二个条件就得琢磨一下,就是线程 //池状态正好等于SHUTDOWN,并且task是空的,并且队列不为空,那 //肯定得再搞个线程去处理队列中剩余的任务吧。反之如果线程池状态大于 //SHUTDOWN,就是要立马关掉,不做任何后续处理,那当然就不需要 //再创建工作线程了。 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { //拿到当前线程池大小 int wc = workerCountOf(c); //1.线程池大小超过理论最大容量 //2.如果要创建的是核心线程,那么要判断核心线程数是否已经满了,非核心线程同理 //只要满足1和2其中一个条件,就返回false,创建失败 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; //如果前面的关卡都过了,说明可以创建,然后提前增加线程池大小 //这里使用了CAS去增加 if (compareAndIncrementWorkerCount(c)) break retry; //如果修改失败了,拿到最新的值 c = ctl.get(); // Re-read ctl //判断一下线程池状态是否变化,如果有变化,就跳到外层循环 //返回继续在内层循环使用CAS尝试增加线程池大小 if (runStateOf(c) != rs) continue retry; } } boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { //创建工作线程,这一步没必要加锁,可以并发创建 w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { final ReentrantLock mainLock = this.mainLock; //下面这里开始加锁,因为涉及到HashSet的读写,它是线程非安全的 mainLock.lock(); try { int rs = runStateOf(ctl.get()); //1.线程池正常运行 //2.线程池状态等于SHUTDOWN并且firstTask为空,意思就是创建一个线程去消耗队列里面的任务 //满足以上任意一个条件都能创建 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { //检查这个线程是否正在运行,因为还没进行到下面t.start(),在这里就运行了的话,明显是不合法的(具体什么场景会碰到,我也不清楚) 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; }
- Q&A
1)为什么需要两层for
循环?
我已经在代码注释写了个大概,第一层循环的作用是判断线程池的状态。第二层循环的作用是判断是否能创建线程,如果能就使用CAS尝试增加线程池的大小。如果失败了,并且线程池状态发生了变化,那么跳到外层循环进行判断处理,反之继续在内层循环重复上述操作。
2)为什么需要使用CAS增加线程池大小,而不直接使用++或+1呢?
这里就引回到execute
源码剖析那里了,还记得上面分析过,如果并发提交任务,会在第一个if分支里,全部进入到addWorker
里,如果这时候不加锁,会出现创建了4个核心线程的情况,使用了CAS操作,能保证只有2个核心线程被创建,其它两个会返回false。
3)为什么需要提前加线程池大小呢?
这样能快速的让那些不速之客离开addWorker
,也能让外面还在想进来的任务死了这条心。
4.工作线程Worker源码剖析
- 代码分析
private final class Worker extends AbstractQueuedSynchronizer implements Runnable { private static final long serialVersionUID = 6138294804551838833L; //工作线程 final Thread thread; //创建Worker的时候,如果传了Task,那么就直接执行这个Task,否则去队列中拿Task Runnable firstTask; //这就是一个统计用的 volatile long completedTasks; Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; //这里创建的线程,传入的Runnable就是Worker本身 //当在外面调用t.start的时候,就会运行Woker中的run方法 this.thread = getThreadFactory().newThread(this); } public void run() { //真正执行的方法 runWorker(this); } //state == 0表示目前没有被线程持有,state == 1表示目前被一个线程持有,相当于被锁住了 protected boolean isHeldExclusively() { return getState() != 0; } //尝试加锁,如果当前state == 0,那么尝试将state置为1,也就是加锁 protected boolean tryAcquire(int unused) { if (compareAndSetState(0, 1)) { //设置当前线程为这个Worker的独占线程(就是排他锁) setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } //释放锁 protected boolean tryRelease(int unused) { setExclusiveOwnerThread(null); setState(0); return true; } public void lock() { acquire(1); } public boolean tryLock() { return tryAcquire(1); } public void unlock() { release(1); } public boolean isLocked() { return isHeldExclusively(); } //这里特有意思,这段代码就是如果线程启动了,就可以中断它了 void interruptIfStarted() { Thread t; if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { try { t.interrupt(); } catch (SecurityException ignore) { } } } }
- Q&A
1)为什么Worker构造函数中有这一行setState(-1)
代码?
看到Woker的interruptIfStarted方法里,有这么一个判断
“getState() >= 0 && (t = thread) != null && !t.isInterrupted()”,state要大于等于0,才能中断这个线程。为什么呢?很好理解嘛,我都还没启动,你就给我中断,这算是人干的事?当外面调用了t.start,线程启动了,state设置为0,这时候你可以中断。
2)为什么runWorker方法在外面?
这个我也没想到,难道仅仅是为了Worker代码尽量简洁?
3)为什么要继承AbstractQueuedSynchronizer,不直接使用ReentrantLock?
我们都知道,ReentrantLock是可重入锁,但是Worker的特性,它不能使用可重入锁,为什么呢?因为它需要实现不可重入的特性去反应线程现在的执行状态。下面分析到interruptIdleWorkers方法的时候,大家就能豁然开朗了。
5.执行任务runWorker源码剖析
- 代码分析
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; //这是防止重复执行这个task w.firstTask = null; //这里就是将state位置0的操作 w.unlock(); // allow interrupts //这个变量也有点意思,考虑得比较全面,这个是防止while循环里面有捕获不到的异常,也就是非正常结束。最后会有相应的处理策略。 boolean completedAbruptly = true; try { //task != null就先执行它,然后才去队列里面拿task //getTask有三种可能,第一个是返回task,第二个是返回null,第三个是一直阻塞(具体的分析getTask源码再讨论) 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(); } } //运行到这里,说明没有未知异常,置为false completedAbruptly = false; } finally { //这就是后续的处理,里面没啥好讨论的,感兴趣可自行分析 processWorkerExit(w, completedAbruptly); } }
- Q&A
1)为什么执行任务前要加锁?
如果单单从这里看,确实没有加锁的必要,这一段就单纯的是同步执行task.run方法,是串行的。如果从全局看,有这么一个方法"interruptIdleWorkers",可以看看它的代码。这里也呼应了上面分析的,为什么要用AQS实现一个不可重入锁,就是为了反应worker的状态。//尝试中断空闲的线程 private void interruptIdleWorkers(boolean onlyOne) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (Worker w : workers) { Thread t = w.thread; //重点来了,这里中断之前,需要判断一下线程是否空闲,怎么判断呢?简单,我们尝试去获取锁,如果获取成功,说明那个线程没有在执行task.run,也就是空闲的了。 if (!t.isInterrupted() && w.tryLock()) { try { t.interrupt(); } catch (SecurityException ignore) { } finally { w.unlock(); } } if (onlyOne) break; } } finally { mainLock.unlock(); } }
6.获取任务getTask源码剖析
-
代码分析
private Runnable getTask() { //这个变量的设计,看一下中篇就知道了,很是巧妙 boolean timedOut = false; for (;;) { int c = ctl.get(); int rs = runStateOf(c); //还是判断线程池状态那一套 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc = workerCountOf(c); //是否允许回收线程,很好理解,如果允许核心线程被回收或者当前线程池大小大于核心线程数,都是允许线程被回收的。 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; //这一段,判断当前线程是否可以回收,如果可以,先将线程池大小减1,并且返回null,外面的循环就会退出了,线程也就退出了 //再来看看具体的条件 //1.当前线程池大小大于最大线程数 //2.允许线程被回收 并且 超过指定时间没获取到任务 并且 (当前线程池中还有至少2个线程 或者 队列为空了) //满足1和2任意一个条件就可以回收线程了 if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { //这里也是用CAS尝试去将线程池大小减1 if (compareAndDecrementWorkerCount(c)) return null; //如果失败了,说明有其它线程成功了,那就重复上述步骤 continue; } try { //允许回收的话,就调用poll,它可以设置超时时间,如果指定时间内没有任务处理,timedOut就为true,说明可以回收了 //反之就调用take一直阻塞下去 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } }
-
Q&A
1)为什么在获取Runnable为null的时候,直接递减线程池大小,然后返回null呢?
如果你自己手撸过线程池,那么应该会遇到这个场景,也就知道为什么了,现在我举个小例子try { boolean timed; try { lock.lock(); //因为这里锁住了,N个线程都会顺序判断一遍,注意,这里会非常快 timed = currentPoolSize.get() > corePoolSize; //if(timed && timeout) { // currentPoolSize.decrementAndGet(); // return null; //} } finally { lock.unlock(); } //这里执行的会比上面的慢一些,会导致什么问题呢? //最坏的情况下,N个线程都直接运行到了这里,并且timed=true //那么会有很多runnable == null Runnable runnable = timed ? workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS) : workerQueues.take(); if(runnable != null){ return runnable; } //这里就会递减过头了,刹不住车 currentPoolSize.decrementAndGet(); return null; //timeout = true; } catch (InterruptedException e) { //timeout = false; }
当然了,办法肯定有的,加锁,判断等等都行,麻烦且效率低下,那还不如递减先于获取来得实在呢。
2)InterruptedException作用是什么?
从名字就可以看出,中断异常,那么作用就很明显了,当调用的是take阻塞的时候,当线程中断的时候,这个就会抛异常。一般什么时候中断线程呢?关闭线程池的时候。
7.线程池shutdown源码剖析
- 代码分析
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); //这就是更新线程池的状态 advanceRunState(SHUTDOWN); //中断闲置的线程,还在执行的任务的让它继续收尾(上面已经分析过这个方法) interruptIdleWorkers(); //开放给子类的方法 onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } //尝试将线程池状态置为TERMINATED,并且还有一些相应的处理 tryTerminate(); } private void advanceRunState(int targetState) { for (;;) { int c = ctl.get(); //很简单的条件,比如targetState == SHUTDOWN //那么c >= SHUTDOWN的话,就不需要修改了 //如果线程池还在运行状态,那么CAS修改它的状态 if (runStateAtLeast(c, targetState) || ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))) break; } } final void tryTerminate() { for (;;) { int c = ctl.get(); //1.对于shutdown这个方法来说,调用这个方法的时候,线程池状态不可能是running了 //2.也不可能是 >= TIDYING,应该是SHUTDOWN //3.如果是SHUTDOWN,并且队列中还有任务,也不行 if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return; if (workerCountOf(c) != 0) { // Eligible to terminate //这里我也不知道为什么只中断一个? interruptIdleWorkers(ONLY_ONE); return; } final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //这段代码就比较简单,就是线程池状态从TIDYING -> TERMINATED //之间,可以给子类一个收尾处理的方法 if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { terminated(); } finally { ctl.set(ctlOf(TERMINATED, 0)); termination.signalAll(); } return; } } finally { mainLock.unlock(); } // else retry on failed CAS } }
- Q&A
1)tryTerminate作用是什么?
这个我的理解,就是一个尝试做最后的收尾工作,给子类开放的一个方法,对于线程池关闭的主流程无任何影响。
8.线程池shutdownNow源码剖析
- 代码分析
public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(STOP); //和shutdown不同的是,不管是不是空闲线程,全部都中断了 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(); } }
- Q&A
1)shutdownNow一定能让线程退出吗?
在这里我要重点说一下,除非程序崩溃,否则没有任何外部方式使线程强制退出,只能是通过将该线程的中断位置为1,然后由线程自行判断决定是否处理这个中断信号。而shutdown和shutdownNow唯一的区别在于,前者只给空闲线程发送中断信号,后者给所有的线程发送。可以看下面这个例子public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>()); for (int i = 0; i < 4; i++) { poolExecutor.execute(() -> { System.out.println("当前线程:" + Thread.currentThread().getName() + " 开始"); while (true){ if(Thread.currentThread().isInterrupted()){ System.out.println("接收到中断信号,退出"); break; } } System.out.println("当前线程:" + Thread.currentThread().getName() + " 结束"); }); } Thread.sleep(2000); System.out.println("线程池关闭"); List<Runnable> runnables = poolExecutor.shutdownNow(); System.out.println(runnables.size()); }
如果使用的是shutdown,那么正在运行的线程,是无论如何都接收不到中断信息的。
三、总结
说实话,线程池的源码我之前看过很多次,但是每次都是懵懵懂懂,然后就是看不下去了,觉得太难了。然后突然兴起,想自己撸一个线程池,看看难度几何,写的过程,遇到很多问题,然后尝试去解决,最后发现有些思想竟然和java线程池一模一样,那些困扰我的问题也一一解开,那种感觉非常棒,哈哈。所以真就应了那一句,实践出真理啊,偷懒最终的苦还是自己承受。