网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
new LinkedBlockingQueue(),
threadFactory);
}
使用示例:
// 1. 创建线程池对象,设置核心线程和最大线程数为5
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 2. 创建Runnable(任务)
Runnable task =new Runnable(){
public void run() {
System.out.println(Thread.currentThread().getName() + “—>运行”);
}
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
**「2、 SingleThreadExecutor」**
单线程线程池。特点是线程池中只有一个线程(核心线程),线程执行完任务立即回收,使用有界阻塞队列(容量未指定,使用默认值`Integer.MAX_VALUE`)
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
// 为节省篇幅,省略了自定义线程工厂方式的源码
使用示例:
// 1. 创建单线程线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建Runnable(任务)
Runnable task = new Runnable(){
public void run() {
System.out.println(Thread.currentThread().getName() + “—>运行”);
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
**「3、 ScheduledThreadPool」**
定时线程池。指定核心线程数量,普通线程数量无限,线程执行完任务立即回收,任务队列为延时阻塞队列。这是一个比较特别的线程池,适用于**「执行定时或周期性的任务」**。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
// 继承了 ThreadPoolExecutor
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor
implements ScheduledExecutorService {
// 构造函数,省略了自定义线程工厂的构造函数
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
// 延时执行任务
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { ... } // 定时执行任务 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {…}
}
使用示例:
// 1. 创建定时线程池
ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建Runnable(任务)
Runnable task = new Runnable(){
public void run() {
System.out.println(Thread.currentThread().getName() + “—>运行”);
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 2, TimeUnit.SECONDS); // 延迟2s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,50,2000,TimeUnit.MILLISECONDS);// 延迟50ms后、每隔2000ms执行任务
**「4、CachedThreadPool」**
缓存线程池。没有核心线程,普通线程数量为`Integer.MAX_VALUE`(可以理解为无限),线程闲置60s后回收,任务队列使用`SynchronousQueue`这种无容量的同步队列。适用于**「任务量大但耗时低」**的场景。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
使用示例:
// 1. 创建缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建Runnable(任务)
Runnable task = new Runnable(){
public void run() {
System.out.println(Thread.currentThread().getName() + “—>运行”);
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
### 解读线程池
OK,相信前面内容阅读起来还算轻松愉悦吧,那么从这里开始就进入深水区了,如果后面内容能吃透,那么线程池知识就真的被你掌握了。
我们知道,向线程池提交任务是用`ThreadPoolExecutor`的`execute()`方法,但在其内部,线程任务的处理其实是相当复杂的,涉及到`ThreadPoolExecutor`、`Worker`、`Thread`三个类的6个方法:
![图片](https://img-blog.csdnimg.cn/img_convert/f9951fa25a74ce14b4b03d1a20b8f3a7.png)
向线程池提交任务
### execute()
在`ThreadPoolExecutor`类中,任务提交方法的入口是`execute(Runnable command)`方法(`submit()`方法也是调用了`execute()`),该方法其实只在尝试做一件事:经过各种校验之后,调用 `addWorker(Runnable command,boolean core)`方法为线程池创建一个线程并执行任务,与之相对应,execute() 的结果有两个:
**「参数说明:」**
1. **「Runnable command」**:待执行的任务
**「执行流程:」**
1、通过 `ctl.get()` 得到线程池的当前线程数,如果线程数小于`corePoolSize`,则调用 `addWorker(commond,true)`方法创建新的线程执行任务,否则执行步骤2;
2、步骤1失败,说明已经无法再创建新线程,那么考虑将任务放入阻塞队列,等待执行完任务的线程来处理。基于此,判断线程池是否处于`Running`状态(只有`Running`状态的线程池可以接受新任务),如果任务添加到任务队列成功则进入步骤3,失败则进入步骤4;
3、来到这一步需要说明任务已经加入任务队列,这时要二次校验线程池的状态,会有以下情形:
* 线程池不再是`Running`状态了,需要将任务从任务队列中移除,如果移除成功则拒绝本次任务
* 线程池是`Running`状态,则判断线程池工作线程是否为0,是则调用 `addWorker(commond,true)`添加一个没有初始任务的线程(这个线程将去获取已经加入任务队列的本次任务并执行),否则进入步骤4;
* 线程池不是`Running`状态,但从任务队列移除任务失败(可能已被某线程获取?),进入步骤4;
4、将线程池扩容至`maximumPoolSize`并调用 `addWorker(commond,false)`方法创建新的线程执行任务,失败则拒绝本次任务。
**「流程图:」**
![图片](https://img-blog.csdnimg.cn/img_convert/bd36bd3bb47774943bbb1920017f8870.png)
创建新的线程执行任务
**「源码详读:」**
/**
* 在将来的某个时候执行给定的任务。任务可以在新线程中执行,也可以在现有的池线程中执行。
* 如果由于此执行器已关闭或已达到其容量而无法提交任务以供执行,则由当前的{@code RejectedExecutionHandler}处理该任务。
*
* @param command the task to execute 待执行的任务命令
/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/
* Proceed in 3 steps:
*
* 1. 如果运行的线程少于corePoolSize,将尝试以给定的命令作为第一个任务启动新线程。
*
* 2. 如果一个任务可以成功排队,那么我们仍然需要仔细检查两点,其一,我们是否应该添加一个线程
* (因为自从上次检查至今,一些存在的线程已经死亡),其二,线程池状态此时已改变成非运行态。因此,我们重新检查状态,如果检查不通过,则移除已经入列的任务,如果检查通过且线程池线程数为0,则启动新线程。
*
* 3. 如果无法将任务加入任务队列,则将线程池扩容到极限容量并尝试创建一个新线程,如果失败则拒绝任务。
*/
int c = ctl.get();
// 步骤1:判断线程池当前线程数是否小于线程池大小
if (workerCountOf© < corePoolSize) {
// 增加一个工作线程并添加任务,成功则返回,否则进行步骤2
// true代表使用coreSize作为边界约束,否则使用maximumPoolSize
if (addWorker(command, true))
return;
c = ctl.get();
}
// 步骤2:不满足workerCountOf© < corePoolSize或addWorker失败,进入步骤2
// 校验线程池是否是Running状态且任务是否成功放入workQueue(阻塞队列)
if (isRunning© && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次校验,如果线程池非Running且从任务队列中移除任务成功,则拒绝该任务
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果线程池工作线程数量为0,则新建一个空任务的线程
else if (workerCountOf(recheck) == 0)
// 如果线程池不是Running状态,是加入不进去的
addWorker(null, false);
}
// 步骤3:如果线程池不是Running状态或任务入列失败,尝试扩容maxPoolSize后再次addWorker,失败则拒绝任务
else if (!addWorker(command, false))
reject(command);
}
### addWorker()
`addWorker(Runnable firstTask, boolean core)` 方法,顾名思义,向线程池添加一个带有任务的工作线程。
**「参数说明:」**
1. **「Runnable firstTask」**:新创建的线程应该首先运行的任务(如果没有,则为空)。
2. **「boolean core」**:该参数决定了线程池容量的约束条件,即当前线程数量以何值为极限值。参数为 `true` 则使用`corePollSize` 作为约束值,否则使用`maximumPoolSize`。
**「执行流程:」**
1、外层循环判断线程池的状态是否可以新增工作线程。这层校验基于下面两个原则:
* 线程池为`Running`状态时,既可以接受新任务也可以处理任务
* 线程池为关闭状态时只能新增空任务的工作线程(`worker`)处理任务队列(`workQueue`)中的任务不能接受新任务
2、内层循环向线程池添加工作线程并返回是否添加成功的结果。
* 首先校验线程数是否已经超限制,是则返回`false`,否则进入下一步
* 通过`CAS`使工作线程数+1,成功则进入步骤3,失败则再次校验线程池是否是运行状态,是则继续内层循环,不是则返回外层循环
3、核心线程数量+1成功的后续操作:添加到工作线程集合,并启动工作线程
* 首先获取锁之后,再次校验线程池状态(具体校验规则见代码注解),通过则进入下一步,未通过则添加线程失败
* 线程池状态校验通过后,再检查线程是否已经启动,是则抛出异常,否则尝试将线程加入线程池
* 检查线程是否启动成功,成功则返回`true`,失败则进入 `addWorkerFailed` 方法
**「流程图:」**
![图片](https://img-blog.csdnimg.cn/img_convert/f3be782d306de51860e44c7d57548e7a.png)
向线程池添加一个带有任务的工作线程
**「源码详读:」**
private boolean addWorker(Runnable firstTask, boolean core) {
// 外层循环:判断线程池状态
retry:
for (;😉 {
int c = ctl.get();
int rs = runStateOf©;
/**
* 1.线程池为非Running状态(Running状态则既可以新增核心线程也可以接受任务)
* 2.线程为shutdown状态且firstTask为空且队列不为空
* 3.满足条件1且条件2不满足,则返回false
* 4.条件2解读:线程池为shutdown状态时且任务队列不为空时,可以新增空任务的线程来处理队列中的任务
*/
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
// 内层循环:线程池添加核心线程并返回是否添加成功的结果
for (;😉 {
int wc = workerCountOf©;
// 校验线程池已有线程数量是否超限:
// 1.线程池最大上限CAPACITY
// 2.corePoolSize或maximumPoolSize(取决于入参core)
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 通过CAS操作使工作线程数+1,跳出外层循环
if (compareAndIncrementWorkerCount©)
break retry;
// 线程+1失败,重读ctl
c = ctl.get(); // Re-read ctl
// 如果此时线程池状态不再是running,则重新进行外层循环
if (runStateOf© != rs)
continue retry;
// 其他 CAS 失败是因为工作线程数量改变了,继续内层循环尝试CAS对线程数+1
// else CAS failed due to workerCount change; retry inner loop
}
}
/**
* 核心线程数量+1成功的后续操作:添加到工作线程集合,并启动工作线程
*/
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
final ReentrantLock mainLock = this.mainLock;
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 下面代码需要加锁:线程池主锁
mainLock.lock();
try {
// 持锁期间重新检查,线程工厂创建线程失败或获取锁之前关闭的情况发生时,退出
int c = ctl.get();
int rs = runStateOf©;
// 再次检验线程池是否是running状态或线程池shutdown但线程任务为空
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 {
//线程启动失败,则进入addWorkerFailed
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
### Worker类
`Worker`类是内部类,既实现了`Runnable`,又继承了`AbstractQueuedSynchronizer`(以下简称`AQS`),所以其既是一个可执行的任务,又可以达到锁的效果。
`Worker`类主要维护正在运行任务的线程的中断控制状态,以及其他次要的记录。这个类适时地继承了`AbstractQueuedSynchronizer`类,以简化获取和释放锁(该锁作用于每个任务执行代码)的过程。这样可以防止去中断正在运行中的任务,只会中断在等待从任务队列中获取任务的线程。
我们实现了一个简单的不可重入互斥锁,而不是使用可重入锁,因为我们不希望工作任务在调用`setCorePoolSize`之类的池控制方法时能够重新获取锁。另外,为了在线程真正开始运行任务之前禁止中断,我们将锁状态初始化为负值,并在启动时清除它(在`runWorker`中)。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
/
private static final long serialVersionUID = 6138294804551838833L;
/* Thread this worker is running in. Null if factory fails. /
final Thread thread;
/* Initial task to run. Possibly null. /
Runnable firstTask;
/* Per-thread task counter /
volatile long completedTasks;
/*
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
/
// 通过构造函数初始化,
Worker(Runnable firstTask) {
//设置AQS的同步状态
// state:锁状态,-1为初始值,0为unlock状态,1为lock状态
setState(-1); // inhibit interrupts until runWorker 在调用runWorker前,禁止中断
this.firstTask = firstTask;
// 线程工厂创建一个线程
this.thread = getThreadFactory().newThread(this);
}
/* Delegates main run loop to outer runWorker /
public void run() {
runWorker(this); //runWorker()是ThreadPoolExecutor的方法
}
// Lock methods
// The value 0 represents the unlocked state. 0代表“没被锁定”状态
// The value 1 represents the locked state. 1代表“锁定”状态
protected boolean isHeldExclusively() {
return getState() != 0;
}
/*
* 尝试获取锁的方法
* 重写AQS的tryAcquire(),AQS本来就是让子类来实现的
/
protected boolean tryAcquire(int unused) {
// 判断原值为0,且重置为1,所以state为-1时,锁无法获取。
// 每次都是0->1,保证了锁的不可重入性
if (compareAndSetState(0, 1)) {
// 设置exclusiveOwnerThread=当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/*
* 尝试释放锁
* 不是state-1,而是置为0
/
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(); }
/*
* 中断(如果运行)
* shutdownNow时会循环对worker线程执行
* 且不需要获取worker锁,即使在worker运行时也可以中断
*/
void interruptIfStarted() {
Thread t;
//如果state>=0、t!=null、且t没有被中断
//new Worker()时state==-1,说明不能中断
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
### runWorker()
可以说,`runWorker(Worker w)` 是线程池中真正处理任务的方法,前面的`execute()` 和 `addWorker()` 都是在为该方法做准备和铺垫。
**「参数说明:」**
1. **「Worker w」**:封装的Worker,携带了工作线程的诸多要素,包括`Runnable`(待处理任务)、`lock`(锁)、`completedTasks`(记录线程池已完成任务数)
**「执行流程:」**
1、判断当前任务或者从任务队列中获取的任务是否不为空,都为空则进入步骤2,否则进入步骤3
2、任务为空,则将`completedAbruptly`置为`false`(即线程不是突然终止),并执行`processWorkerExit(w,completedAbruptly)`方法进入线程退出程序
3、任务不为空,则进入循环,并加锁
4、判断是否为线程添加中断标识,以下两个条件满足其一则添加中断标识:
* 线程池状态>=`STOP`,即`STOP`或`TERMINATED`
* 一开始判断线程池状态<`STOP`,接下来检查发现`Thread.interrupted()`为`true`,即线程已经被中断,再次检查线程池状态是否>=`STOP`(以消除该瞬间`shutdown`方法生效,使线程池处于`STOP`或`TERMINATED`)
5、执行前置方法 `beforeExecute(wt, task)`(该方法为空方法,由子类实现)后执行`task.run()` 方法执行任务(执行不成功抛出相应异常)
6、执行后置方法 `afterExecute(task, thrown)`(该方法为空方法,由子类实现)后将线程池已完成的任务数+1,并释放锁。
7、再次进行循环条件判断。
**「流程图:」**
![图片](https://img-blog.csdnimg.cn/img_convert/a4a2e4d81bca6b6922e1247f347e0fc1.png)
线程池流程图
**「源码详读:」**
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// allow interrupts
// new Worker()是state==-1,此处是调用Worker类的tryRelease()方法,将state置为0,而interruptIfStarted()中只有state>=0才允许调用中断
w.unlock();
// 线程退出的原因,true是任务导致,false是线程正常退出
boolean completedAbruptly = true;
try {
// 当前任务和从任务队列中获取的任务都为空,方停止循环
while (task != null || (task = getTask()) != null) {
//上锁可以防止在shutdown()时终止正在运行的worker,而不是应对并发
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
/**
* 判断1:确保只有在线程处于stop状态且wt未中断时,wt才会被设置中断标识
* 条件1:线程池状态>=STOP,即STOP或TERMINATED
* 条件2:一开始判断线程池状态<STOP,接下来检查发现Thread.interrupted()为true,即线程已经被中断,再次检查线程池状态是否>=STOP(以消除该瞬间shutdown方法生效,使线程池处于STOP或TERMINATED),
* 条件1与条件2任意满意一个,且wt不是中断状态,则中断wt,否则进入下一步
*/
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt(); //当前线程调用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++; //完成任务数+1
w.unlock(); //释放锁
}
}
//
completedAbruptly = false;
}
finally {
//处理worker的退出
processWorkerExit(w, completedAbruptly);
}
}
**「5、getTask()」**
由函数调用关系图可知,在`ThreadPoolExecutor`类的实现中,`Runnable getTask()` 方法是为`void runWorker(Worker w)`方法服务的,它的作用就是在任务队列(`workQueue`)中获取 task(`Runnable`)。
**「参数说明」**:无参数
**「执行流程」**:
1. 将`timedOut`(上次获取任务是否超时)置为`false`(首次执行方法,无上次,自然为`false`),进入一个无限循环
2. 如果线程池为`Shutdown`状态且任务队列为空(线程池`shutdown`状态可以处理任务队列中的任务,不再接受新任务,这个是重点)或者线程池为`STOP`或`TERMINATED`状态,则意味着线程池不必再获取任务了,当前工作线程数量-1并返回`null`,否则进入步骤3
3. 如果线程池数量超限制或者时间超限且(任务队列为空或当前线程数>1),则进入步骤4,否则进入步骤5。
4. 移除工作线程,成功则返回`null`,不成功则进入下轮循环。
5. 尝试用`poll()` 或者 `take()`(具体用哪个取决于`timed`的值)获取任务,如果任务不为空,则返回该任务。如果为空,则将`timeOut` 置为 `true`进入下一轮循环。如果获取任务过程发生异常,则将 `timeOut`置为 false 后进入下一轮循环。
**「流程图」**:
![图片](https://img-blog.csdnimg.cn/img_convert/98857b98c632fbd22a8d9807c4616008.png)
线程池流程图
**「源码详读:」**
private Runnable getTask() {
// 最新一次poll是否超时
boolean timedOut = false; // Did the last poll() time out?
for (;😉 {
int c = ctl.get();
int rs = runStateOf©;
// Check if queue empty only if necessary.
/**
* 条件1:线程池状态SHUTDOWN、STOP、TERMINATED状态
* 条件2:线程池STOP、TERMINATED状态或workQueue为空
* 条件1与条件2同时为true,则workerCount-1,并且返回null
* 注:条件2是考虑到SHUTDOWN状态的线程池不会接受任务,但仍会处理任务
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf©;
// Are workers subject to culling?
/**
* 下列两个条件满足任意一个,则给当前正在尝试获取任务的工作线程设置阻塞时间限制(超时会被销毁?不太确定这点),否则线程可以一直保持活跃状态
* 1.allowCoreThreadTimeOut:当前线程是否以keepAliveTime为超时时限等待任务
* 2.当前线程数量已经超越了核心线程数
*/
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 两个条件全部为true,则通过CAS使工作线程数-1,即剔除工作线程
// 条件1:工作线程数大于maximumPoolSize,或(工作线程阻塞时间受限且上次在任务队列拉取任务超时)
// 条件2:wc > 1或任务队列为空
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 移除工作线程,成功则返回null,不成功则进入下轮循环
if (compareAndDecrementWorkerCount©)
return null;
continue;
}
// 执行到这里,说明已经经过前面重重校验,开始真正获取task了
try {
// 如果工作线程阻塞时间受限,则使用poll(),否则使用take()
// poll()设定阻塞时间,而take()无时间限制,直到拿到结果为止
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// r不为空,则返回该Runnable
if (r != null)
return r;
// 没能获取到Runable,则将最近获取任务是否超时设置为true
timedOut = true;
} catch (InterruptedException retry) {
// 响应中断,进入下一次循环前将最近获取任务超时状态置为false
timedOut = false;
}
}
}
### processWorkerExit()
`processWorkerExit(Worker w, boolean completedAbruptly)`执行线程退出的方法
**「参数说明:」**
1. **「Worker w」**:要结束的工作线程。
2. **「boolean completedAbruptly」**:是否突然完成(异常导致),如果工作线程因为用户异常死亡,则`completedAbruptly`参数为 `true`。
还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!
王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。
对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!
【完整版领取方式在文末!!】
***93道网络安全面试题***
![](https://img-blog.csdnimg.cn/img_convert/6679c89ccd849f9504c48bb02882ef8d.png)
![](https://img-blog.csdnimg.cn/img_convert/07ce1a919614bde78921fb2f8ddf0c2f.png)
![](https://img-blog.csdnimg.cn/img_convert/44238619c3ba2d672b5b8dc4a529b01d.png)
内容实在太多,不一一截图了
### 黑客学习资源推荐
最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
#### 1️⃣零基础入门
##### ① 学习路线
对于从来没有接触过网络安全的同学,我们帮你准备了详细的**学习成长路线图**。可以说是**最科学最系统的学习路线**,大家跟着这个大的方向学习准没问题。
![image](https://img-blog.csdnimg.cn/img_convert/acb3c4714e29498573a58a3c79c775da.gif#pic_center)
##### ② 路线对应学习视频
同时每个成长路线对应的板块都有配套的视频提供:
![image-20231025112050764](https://img-blog.csdnimg.cn/874ad4fd3dbe4f6bb3bff17885655014.png#pic_center)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以点击这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**