什么是多线程?
多线程是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。线程是在同一时间需要完成多项任务的时候被实现的。其实如果学过操作系统的同学的话就知道,其实我们操作底层中的cpu是以时间片的方式供这些线程使用,其实实时每次只有一个线程去使用cpu,但是时间片的间隔很小,所以让人以为是并行的。
什么时候需要使用多线程?
其实使用多线程的场景很多,比如你有好多任务需要做,假如使用单线程,那么这些就要一一等待,而多线程就可以“并行”执行,还有就是对于有些耗时的操作,比如上传文件,这时候我们应该开启线程去执行,否则会柱塞主线程,其实这个在安卓上面就能完全体现出来(如果你访问网络的时候不新开线程就会报错)。反正需要使用多线程的场景就是任务多或者耗时较长的,这些场景都可以使用多线程。
多线程与线程池
如果大家学过web的话,就应该知道其实在web上面有好多这种池,比如数据库连接池、redis连接池,他们的作用其实都是一样的,目的就是减少每次开启和关闭的时间。其实我们线程池也类似这样的,其实大家可以想想,其实我们每次开启一个线程去执行任务的时候,其实他正儿八经执行任务的时间很少,大多都是花费在开启和销毁下,如果并发量大的话,每个连接都需要开启和关闭的话,那么我们的服务器性能一定会下降很多,因此我们就需要一个池来装这些多线程,默认可以开启一些线程,然后又需要的有直接取,用完就扔回池里,下一个需要使用线程的可以直接去池里拿,我们无需关系他的开启和销毁,我们还可以根据实际的需求调整线程池的大小等参数。
至于多线程怎么用,大家这个可以自己百度解决,而我们本节主要讲的是线程池底层是怎么实现的,也就是我们的我们将任务放进去他是怎么执行的,以及当线程池的大小超过某个阈值的时候他是怎么处理的。好了我们就直接进入今天的主题
ThreadPoolExecutor源码解析
首先我们要了解下ThreadPoolExecutor的结构,他的一个继承关系是这样的
其实我们从上面的图就能看到,顶层接口Excutor其实就只定义了一个方法
void execute(Runnable command);
而我们平常使用的几个常用的线程池其实可以理解为就是通过工厂模式来实现的,
对比这两个就知道了,其实他们都是ExecutorService的之类,其实底层也是通过我们ThreadPoolExecutor来实现的。
说到这来我们有必要先讲解一下线程池里面的几个重要的参数,可能你使用线程池没有用过这么多的参数(实时上你没有设置的时候他就已经帮你设置好了,其实上面两个图你就知道了)。因为我们这次主要讲的是ThreadPoolExecutor,那四种都是相对于ThreadPoolExecutor来实现的。我们就会先去看ThreadPoolExecutor需要哪些参数:
- corePoolSize:核心线程数吗,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
- maximumPoolSize:最大活动线程数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:空闲时间,表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:这个表示时间的单位。这个是个枚举类
- workQueue:工作队列,当我们线程达到核心线程数的时候,这时候会将进来的任务放到我们这个工作队列中。这边一般有一下的取值可能
- ArrayBlockingQueue;
- LinkedBlockingQueue;
- SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
- RejectedExecutionHandler:这个就是拒绝新任务应该执行的方法,我们上面说当线程达到核心线程的数量的时候,那么剩下的工作任务会放进队列中,但是当队列满了的话,在进入的任务会继续开启一个线程去执行,只要当前的线程不要大于最大活动线程,如果超过了,这时候在新进入任务就会直接将他给拒绝。他也有自己的策略,主要是以下几种
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
讲解完我们上面的这几个参数的时候,我们就要去开始解读他的源码,其实我们之前看他的结构就知道,execute方法是很重要的,还有就是submit(),其实submit是要去调用execute方法的。其实我们自己写一个demo的时候就会发现,
我们每一次将线程放入submit中,还有就是shutdown和shutdownNow方法。
首先还是先看submit方法,这个方法的实现是在AbstractExecutorService抽象类中实现的,所以我们是直接使用它的。
public Future<?> submit(Runnable task) {
//这里传入了一个runnable的任务
if (task == null) throw new NullPointerException();
//将这个task进一步封装,将我们的Runnable转化为RunnableFuture
RunnableFuture<Void> ftask = newTaskFor(task, null);
//然后执行execute方法,这个很重要
execute(ftask);
return ftask;
}
我们继续看execute(ftask)方法,这个的具体实现就在threadPoolExecutor类里面实现的
public void execute(Runnable command) {
//先判断这个Runnable是否为null
if (command == null)
throw new NullPointerException();
//获取当前的工作线程的数量
int c = ctl.get();
//如果工作线程小于我们开始设置的核心线程数,
if (workerCountOf(c) < corePoolSize) {
//就将我们这个任务放入新开线程运行,如果返回false,就继续执行下面的
if (addWorker(command, true))
return;
//再次获取32位的值
c = ctl.get();
}
//判断线程池是否还在运行,并且排队成功,因为到这里我们认为活动线程是大于核心线程的,所以要让他加入队列等待,加入成功就返回true
if (isRunning(c) && workQueue.offer(command)) {
//继续获取32位的值,然后再次进行判断
int recheck = ctl.get();
//非RUNNING状态 则从workQueue中移除任务并拒绝
if (! isRunning(recheck) && remove(command))
// 采用线程池指定的策略拒绝任务
reject(command);
//线程池处于RUNNING状态 || 线程池处于非RUNNING状态但是任务移除失败
else if (workerCountOf(recheck) == 0)
/ 这行代码是为了SHUTDOWN状态下没有活动线程了,但是队列里还有任务没执行这种特殊情况。
// 添加一个null任务是因为SHUTDOWN状态下,线程池不再接受新任务
addWorker(null, false);
//1.非RUNNING状态拒绝新的任务,队列满了启动新的线程失败(workCount > maximumPoolSize),传入的是false,然后就会去判断是否为最大的活动线程数
else if (!addWorker(command, false))
reject(command);
}
到上面的时候我们就需要去了解以下的这几个东西了
这几个参数其实我上面是有解释的,但是还是需要解释一下,我们看到这几个参数都是32位的,开始可能看都看不懂,我也是,后面发现他使用的是高3位和低29位,也就是将高3位存线程池的状态,后29位存活动线程的个数。然后下面的几个方法通过与或方法来获取这个32位的数据。
上面提到一个addWorker方法,我们直接看这个方法的代码,这个代码很长,但是也不难理解,我这边代码都会注释
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
//这个跟我们aqs里面的一样,使用自旋操作
for (;;) {
//获取当前工作线程数量和线程池状态
int c = ctl.get();
int rs = runStateOf(c);
//如果现在状态是SHUTDOWN,但是firstTask不为空或者workQueue为空的话,那么直接返回false。因为线程池已经停止了,就没必要执行任务了
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
//获取当前工作线程的数量(线程池中的),如果已经大于核心数量的时候我们直接返回false,因为要将这个任务加入队列中,因为我们开始传进来的core为true
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//如果上面都不是,那么证明这个是可以新开一个线程去执行任务的,这时候就通过cas的方法将线程池的线程数+1,然后跳出这个retry循环
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
}
}
//跳出上面的自旋会执行下面的这些代码,,默认开始和增加都为false
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//将传进入的firstTask放入Worker中,其实这个Worker做的事情也是很简单,如下图
w = new Worker(firstTask);
//获取我们刚才在worker中生成的线程
final Thread t = w.thread;
//如果不为null的话
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
//加锁
mainLock.lock();
try {
//在次去判断线程池的运行状态,如果线程池还活着的就是小于0,等于0,代表不能在往里面添加东西,
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();
//如果大于最大的话,更新线程池的数据,那么就将largestPoolSize =s,然后将workerAdded 置位true,这个可是至关重要的,因为我们下面的线程的启动就是依赖他的
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
//释放锁供其他线程使用
mainLock.unlock();
}
if (workerAdded) {
//这个如果为true,就启动线程,也就是可以开始工作
t.start();
//然后将workerStarted也设置为true
workerStarted = true;
}
}
} finally {
//如果失败的话,其实就是将她从work中移除
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
我们如果失败的话也就是会执行addWorkerFailed这个方法
private void addWorkerFailed(Worker w) {
//加锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
如果不为null的话就将w从workers移除
if (w != null)
workers.remove(w);
//将workerCount的数量-1
decrementWorkerCount();
//然后调用tryTerminate停止线程池,并且返回false。
tryTerminate();
} finally {
mainLock.unlock();
}
}
到此为止线程池的基本也解析完成了,下面我会解析一下shutdown和shutdownNow,他主要是打断线程池的。
shutdown()方法
public void shutdown() {
//获取锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);//转换线程池状态为SHUTDOWN
interruptIdleWorkers();//中断所有空闲的线程
onShutdown(); //空实现方法,是做shutdown清理操作的
} finally {
mainLock.unlock();
}
tryTerminate();//尝试结束线程池(设置状态为TERMINATED)
}
他的作用是暂停线程池,但是他会等所有任务执行完成后才会打断线程池。
shutdownNow方法
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);//转换线程池状态到STOP
interruptWorkers();//中断所有线程
tasks = drainQueue();//获取到任务队列所有任务,并清空队列
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
shutdownNow方法是立即执行停止线程池的方法,即使线程还在执行,他也会直接将正在执行的线程给打断