线程池的学习
学习中…
如果出现错误,希望指正。
1. 线程池?
在java中线程的实现和java虚拟机的规范无关,与具体的虚拟机实现线程的方式有关。
现在的java虚拟机(JDK1.3起)中:创建的线程都是与操作系统中的线程相对应,即java创建一个线程直接映射到一个操作系统原生线程来实现的,所以java中的线程调度都交给操作系统来负责了。
既然java线程与操作系统原生线程一一对应,所以创建线程需要 「java虚拟机」和「操作系统」相互配合完成线程的创建。如栈内存分配、操作系统上创建和注册本地线程等。
每使用一个线程都要进行:创建和销毁。
单个线程可能不算啥,但是抵不住创建的线程多。并且线程创建多了,我们管理维护起来也麻烦。而如果已经创建的线程在执行完一个任务之后,下次新的认为仍然继续使用这个线程该多好,同时要是有个东西来帮我们同一管理维护创建出来的线程该多好。
举个不太合理的例子:
- A想骑自行车🚴 → 买一辆 → 没钱买不起😭,即使买了还要自己看着别丢了,太麻烦了。
- 共享单车公司B🏬 → 买啥呢!我这有已经有一堆自行车,你看 🚲🚲🚲🚲🚲🚲🛵🚲🚲🚲🚲还有电动车 → 用一次还很便宜,也不用你管理,多方便
🚲:线程
🏬:线程池
java实现上面的需求的就是线程池。实现线程的创建、复用、维护、管理。线程池减少了线程的频繁创建和销毁,这降低系统资源的消耗。当上一个任务执行完成之后,下一个任务无需等待线程的创建就可以复用线程来执行任务,这提高了系统的响应速度。同时可以做到对创建的线程进行集中管理。上述可以说是使用线程池的好处。
2. JUC中线程池类结构
线程池的相关类的继承和实现关系如下图:
图中的虚线为实现接口
图中的实线为继承
-
上图中有一个独立的类「Executors」类:它是线程池的静态工厂类,里面有一些创建特定参数线程池的static方法。如其里面的4大创建方式:
1. newSingleThreadExecutor:创建只有一个线程的线程池 2. newFixedThreadPool:创建固定数量线程的线程池 3. newCachedThreadPool:创建根据异步任务情况创建线程的线程池 4. newScheduledThreadPool:创建具有延迟调度的线程池
后续对这些方法进行详细的学习。
-
Executor接口:线程池的顶级接口,内部定义了一个唯一的方法,用于线程任务的提交
void execute(Runnable command);
-
ExecutorService接口:继承自Executor接口,在父接口上又提供了一些用于任务提交的方法(可以接收Callable任务)
// 单个任务提交 <T> Future<T> submit(Callable<T> task); Future<?> submit(Runnable task); // 批量提交任务 <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
-
AbstractExecutorService类:继承自ExcutorService接口,对部分方法进行了实现。
-
ThreadPoolExecutor类:是线程池的核心实现类
submit方法内部实现时会将提交的任务交给execute方法进行执行。 -
ScheduledExecutorService接口:继承自ExcutorService接口,内部又定义了一些与延时、周期性任务相关的方法。
-
ScheduledThreadPoolExecutor类:在ThreadPoolExecutor的功能基础上实现延时、定时的功能的线程池类。
3. ThreadPoolExecutor线程池类的7大参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, TimeUnit unit, // 空闲线程的线程存活时间和单位
BlockingQueue<Runnable> workQueue, // 任务阻塞队列
ThreadFactory threadFactory, // 线程创建工厂
RejectedExecutionHandler handler // 任务拒绝策略
)
3.1 前4个参数
-
「核心线程数」和「最大线程数」:这两个参数控制线程池中创建和存活的线程数量。
比较好线程数参数确定方法可根据任务类型进行设置,但并没有明确的公式可以计算一个很合理的参数:
① IO密集型任务: 2 ∗ c p u 核 数 {2*cpu核数} 2∗cpu核数。IO操作的时间通常是比较常的,大量的IO操作将导致CPU的利用率不高,所以多开点线程,尽可能提高一下CPU利用率。
java中可以使用Runtime.getRunTime.availableProcessors()
获取JVM可用的CPU核数。这个返回的CPU核数并不准确,但看很多代码使用该值当做CPU核数。一句很好的描述这个方法的话:“虚拟机其实不知道什么是处理器,它只有去请求操作系统返回一个值,同样的,操作系统也不知道怎么回事,它是去问硬件设备,硬件设备告诉它一个值,通常来说是硬件线程数。操作系统相信硬件说的,虚拟机又相信操作系统说的”[1]。② 计算密集型任务: c p u 核 数 {cpu核数} cpu核数。
该类型需要使用CPU进行大量的运算(如视频处理),可以很大程度上利用CPU,但是如果创建的线程太多,多个任务之间线程不断切换,这样反而使CPU执行任务的效率变低,因此线程数不能过多,一般设置为CPU的核数比较好。
③ 混合型任务: 线 程 等 待 时 间 + 线 程 使 用 C P U 计 算 时 间 线 程 使 用 C P U 计 算 时 间 ∗ C P U 核 数 {\cfrac{线程等待时间 + 线程使用CPU计算时间}{线程使用CPU计算时间}}{* CPU核数} 线程使用CPU计算时间线程等待时间+线程使用CPU计算时间∗CPU核数
公式特点在于权衡了「线程等待时间」和「线程使用CPU计算时间」。等待时间越长设置的线程数就越多;用于计算的时间越多,设置的线程数就与少。
-
「空闲线程的存活时间」和「时间单位」
在默认情况下,「空闲线程的存活时间」仅仅对超过「核心线程数」以外的线程起作用,即对非核心线程起作用,在非核心线程的空闲时间超过了指定的时间后,线程将会被回收。这个「存活时间」在线程池运行的过程中是可以进行修改的:
// ThreadPoolExecutor.java public void setKeepAliveTime(long time, TimeUnit unit)
刚刚提到在默认情况下「空闲线程的存活时间」仅仅对 非核心新线程 起作用,那有没有办法让其对核心线程也起作用呢?可以做到,仅仅需要调用下面方法即可:
// ThreadPoolExecutor.java // 传入true时,将允许「空闲线程存活时间」对核心线程也起作用 // 即 核心线程 空闲一定时间后,也将被回收 public void allowCoreThreadTimeOut(boolean value)
线程池中「核心线程」和「非核心线程」的理解[2]:
这两个都是虚拟的概念,在线程池中并没有具体指明哪个线程必须是「核心线程」哪个线程必须是「非核心线程」,所有的线程都是一样的,只是线程池中的线程的数量多于指定的 核心线程数量 时,会将多出的部分称为「非核心线程」而已,线程在进行销毁时,被销毁的线程是随机进行的,可能是第一个创建出来的线程,也可能是最后创建出来的线程或是其他时候创建出来的线程。
错误的理解:线程创建时就被标记为「核心线程」在进行销毁时仅仅是将没有标记为「核心线程」的线程销毁。
3.2 任务阻塞队列
在线程池创建时需要指定一个保存任务的阻塞队列,阻塞队列和普通的队列相比,一个明显的特点是:当阻塞队列为空时,获取元素队列中元素操作的线程将会被阻塞,直到队列中有元素后,被阻塞的线程才会被唤醒(我们不用写代码唤醒阻塞的线程)。
创建线程池时,常用的阻塞队列有:
1. ArrayBlockingQueue
2. LinkedBlockingQueue
3. PriorityBlockingQueue
4. DelayQueue
5. SynchronousQueue
这点知识不全,后续学习补充。。。
3.3 线程创建工厂
线程池在创建线程的时候调用该工厂方法创建线程。线程池在添加任务需要创先新线程时大简单流程如下图:
从上面可以看到线程的创建最终调用的是ThreadFactory中唯一的一个方法——newThread方法:
public interface ThreadFactory {
Thread newThread(Runnable r);
}
所以ThreadFactory要做的唯一一件事情就是创建线程。我们通过实现ThreadFactory接口中的newThread()方法,完全可以按照自己的意愿创建线程,如更改所创建的线程的名称、线程组、新线程栈区大小、将线程设置为守护线程、更改线程的优先级等。只要是线程创建时和创建之后能够进行变动的,都可以在这个工厂方法中进行组合定制,创建出符合自己意愿的线程。
在使用默认的线程创建工厂时线程的名称是这样的:
import java.util.concurrent.*;
public class ThreadFactoryTest {
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 5,
50, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1));
for (int i = 0; i < 2; i++) {
poolExecutor.execute(() -> {
// 输出线程名称
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池,该方法不会将线程池立即关闭,会将线程中的任务执行完成之后再关闭线程池
poolExecutor.shutdown();
}
}
// 上面代码的输出结果为:
pool-1-thread-1
pool-1-thread-2
下面自定义一个简单的线程创建工厂类,实现线程名称的自定义,示例代码如下:
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadFactoryTest {
// 自定义创建工厂类:实现线程名称的自定义
static class ThreadFactoryDemo implements ThreadFactory
private static AtomicInteger atoNo = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
// 自定义线程名称
return new Thread(r, "ThreadFactoryDemo_Thread——" + atoNo.getAndIncrement());
}
}
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 5,
50, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1),
new ThreadFactoryDemo()); // 使用自定义的线程创建工厂
for (int i = 0; i < 2; i++) {
poolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
poolExecutor.shutdown();
}
}
// 上面代码的输出结果为:
ThreadFactoryDemo_Thread——1
ThreadFactoryDemo_Thread——2
3.4 拒绝策略
向线程池中提交任务时,如果遇到下面任意一种情况时都会触发拒绝策略的执行:
- ① 线程池已经被关闭时
- ② 阻塞队列已满且工作的线程数已经达到最大线程数时
触发拒绝策略后会调用对应的拒绝方法:RejectedExecutionHandler接口中定义的rejectedExecution方法的具体实现。JUC中提供了四种拒绝策略,我们也可以通过实现接口自定义拒绝策略。
先对JUC中已有的四种拒绝策略进行介绍,如下表:
拒绝策略
| 特点 | 应用场景[3] | 拒绝时是否有异常抛出 |
---|---|---|---|
AbortPolicy | 为默认的拒绝策略,在拒绝任务时会抛出RejectedExecutionException异常告知用户程序。注意做好异常的处理 | 如果是比较关键的业务,推荐使用该策略,这样系统不能承载更大的并发任务量时,能够及时的通过异常发现 | 有异常抛出 |
DiscardPolicy | 可以认为是AbortPolicy 策略的安静版本,看实现代码可以发现,其拒绝方法为一个空方法。所以在拒绝任务时不会抛出任何异常 | 适用于提交的任务为无关紧要的任务。该策略其实基本用不上 | 无异常抛出 |
DiscardOldestPolicy | 拒绝任务时,将会丢弃老任务,即最早进入阻塞队列进行等待的任务,即队列头部元素丢弃掉,之后再尝试将新任务加入队尾。 | 这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,想到的场景就是,发布消息和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较 | 有异常抛出 |
callerRunsPolicy | 向线程池添加新任务被拒绝时,那么提交任务的线程将自己去执行该任务,不会使用线程池去执行该任务,这样一方面避免了任务的丢失,另一方面使任务提交者忙起来,降低向线程池提交任务的速度,进而减轻线程池的压力。 | 应用在不允许任务失败的、对性能要求不高、并发量小的场景 | 无异常抛出 |
除了上面介绍的已有的4中拒绝策略,我们可以自定义拒绝策略,在这之前,我们可以看一下RejectedExecutionHandler接口:
public interface RejectedExecutionHandler {
// 拒绝方法
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
我们自定义拒绝策略是只需要实现RejectedExecutionHandler 接口中的拒绝方法即可:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class RejectedPolicyTest {
// 自定义拒绝策略
static class RejectedPolicyDemo implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 在这可以对被拒绝的任务做一些记录和处理工作
System.out.println(r + "被拒绝。" + "线程池中现在已有的任务数量为:" + executor.getTaskCount());
}
}
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 3,
50, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1),
new RejectedPolicyDemo()); // 自定义拒绝策略
for (int i = 0; i < 10; i++) {
poolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(10000); // 避免提交的任务结束太早
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
poolExecutor.shutdown();
}
}
// 输出结果为
com.booklearn.RejectedPolicyTest$$Lambda$1/1831932724@7ba4f24f被拒绝。线程池中现在已有的任务数量为:4
com.booklearn.RejectedPolicyTest$$Lambda$1/1831932724@7ba4f24f被拒绝。线程池中现在已有的任务数量为:4
com.booklearn.RejectedPolicyTest$$Lambda$1/1831932724@7ba4f24f被拒绝。线程池中现在已有的任务数量为:4
com.booklearn.RejectedPolicyTest$$Lambda$1/1831932724@7ba4f24f被拒绝。线程池中现在已有的任务数量为:4
com.booklearn.RejectedPolicyTest$$Lambda$1/1831932724@7ba4f24f被拒绝。线程池中现在已有的任务数量为:4
com.booklearn.RejectedPolicyTest$$Lambda$1/1831932724@7ba4f24f被拒绝。线程池中现在已有的任务数量为:4
pool-1-thread-3
pool-1-thread-1
pool-1-thread-2
pool-1-thread-2
上面我们提交了10个任务,且每一个任务都需要较长的处理实现,这样线程提交的速度就大于线程任务处理的速度,但是线程池中最多可以容纳4个线程任务(最大线程数+阻塞队列容量)。所以就出现了6个任务被拒绝,从而调用我们自定义的拒绝方法执行。
4. 线程池对任务调度流程
上面是对线程池的7大参数的学习,而关于线程池对于任务的处理是怎么样的,将在这里进行介绍。主要是介绍任务提交时,线程池是执行线程呢?还是将任务加入队列呢?还是拒绝任务呢?
处理流程图如下:
① 如果当前线程池中线程数(包括空闲的)小于核心线程数,则优先创建线程执行新提交的任务,而不是选择一个空闲的线程去执行新提交的任务。不满足要求时转到②执行
② 如果当前线程池中线程数(包括空闲的)大于等于 核心线程数,会向阻塞队列中添加任务,直到阻塞队列满。在阻塞队列未满时,是不会创建新线程来处理添加的任务。当有线程处理完成后,会先从阻塞队列中获取下一个任务执行,直到队列为空。
③ 在核心线程用完且阻塞队列已满情况下,如果线程池接收到新的任务,线程池将创建新的线程(非核心线程)来处理新任务。
④ 在核心线程用完且阻塞队列已满情况下,对于新任务会一直创建新线程,直到线程池内的线程数超过最大线程数。如果线程数超过最大线程数,线程池对于新任务将执行拒绝策略。
总结:线程池会优先创建出和使用「核心线程数」数量内的的线程执行新提交的任务,在「核心线程数」数量的线程都使用完成之后,使用阻塞队列进行任务缓存。当前两者都使用完了再在「最大线程数」允许的范围内创建新的线程执行新提交的任务。只要有空闲线程和阻塞队列有空闲,就不会通过创建新线程执行新提交的任务。
5. 线程池工具类Executors中创建线程池的方法及存在的问题
在开始的时候提到了线程池工具类Executors中提供了与线程池创建相关的静态方法。但是在开发过程中很多公司是静止使用的,这是因为其存在一些潜在问题。
推荐使用ThreadPoolExecutor类创建,仅仅有7个参数,也不是很麻烦。
下图中列举了Executors类中创建线程池的4个方法及存在的问题:
上图中红色的区域是对应创建方式存在潜在问题的地方。
另外需要注意的是newSingleThreadExecutor类,这个类对使用ThreadPoolExecutor创建出的对象进行了一次包装,其他的3个创建方式并没有进行包装处理,直接返回创建的对象:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService( // 外层多包了一个类
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
);
}
上面代码中可以看出创建 单线程线程池 的方法中在ThreadPoolExecutor对象外包了一个FinalizableDelegatedExecutorService类,可以看一下类的继承关系图:
先在这里说一下使用FinalizableDelegatedExecutorService类包装的原因吧:防止创建出的线程池的「核心线程数」和「最大线程数」参数被修改,从而保证创建的线程池中始终只有一个线程。
newSingleThreadExecutor返回的其实是FinalizableDelegatedExecutorService实例的向上转型后的结果,通过上图的类关系图中可以看出,该实例是无法向下转型为ThreadPoolExecutor实例,这就保证了所创建出的线程池对象时没有办法调用ThreadPoolExecutor中定义的修改线程数量的方法setCorePoolSize和setMximumPoolSize方法,这样进而保证了线程池的线程数量始终为保持为1,无法进行修改。
下面我们可以进行一下测试:
// 通过Executors创建单线程线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();
System.out.println(singlePool instanceof ThreadPoolExecutor); // false
6. 线程池提交线程任务的方法
execute、submit、invokeAll、invokeAny
-
方式一:execute方法
// 定义在Executor接口中定义 void execute(Runnable command);
-
方式二:submit方法
// ExecutorService接口中定义 1. Future<?> submit(Runnable task); 2. <T> Future<T> submit(Runnable task, T result); 3. <T> Future<T> submit(Callable<T> task);
execute方法和submit方法的区别:
区别点execute submit 接收参数区别 只能接收Runnable类型参数 可以接收Runnable、Callable两种类型参数 是否有返回值区别 无返回值,即调用者并不关系线程的执行结果 有返回值,返回一个Future子类实例,通过Future子类实例可以获取线程任务的执行结果 异常处理能力区别 线程任务执行过程中可能发生的异常调用者并不关心 可以将线程任务中发生的异常进行保存 submit方法提交的任务,在代码实现上最终是通过execute方法进行执行的,大概流程如下图:
FutureTask,是Future接口的子类,Future接口中提供了三类功能的方法:
① 取消任务cancel
、isCancelled
② 任务是否执行完成isDone
③ 获取任务执行结果get
在返回值和异常方面,execute和submit的区别中,submit是将任务返回值或运行中的异常都是通过存储在Future子类实例当中。通过调用Future中的get方法来阻塞的获取线程任务执行的结果或捕获线程任务执行过程中发生的异常。
说到异常,延伸一个疑问:如果线程池中提交的任务在执行的过程中出未捕获的异常时会发生什么?出现异常的线程将会被销毁,线程池有新任务时会重新创建新的线程。
为了简单的模拟线程内出现未捕获的异常时线程的变化,我们创建的线程池为 核心线程数 = 最大线程数 = 1,阻塞队列为128(保证足够测试的容量即可)。
同时清留意我们测试时使用的线程任务提交方法先来看一下提交正常执行的任务的执行机构的例子:
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolException_9 { public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.NANOSECONDS, new LinkedBlockingQueue<>(128)); for (int i = 0; i < 5; i++) { pool.execute(() -> { // 提交能正常执行的任务 int a = 4 / 2 ; System.out.println(Thread.currentThread().getName()); }); try { // 每2s提交一个任务 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } pool.shutdown(); }
执行结果为:
pool-1-thread-1 pool-1-thread-1 pool-1-thread-1 pool-1-thread-1 pool-1-thread-1
下面通过提交未捕获运行时异常的任务例子,看一下结果输出:
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolException_9 { public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.NANOSECONDS, new LinkedBlockingQueue<>(128)); for (int i = 0; i < 5; i++) { pool.execute(() -> { // 提交存在未捕获运行时异常的任务 int a = 4 / 0; // 运行时异常:除0运算 System.out.println(Thread.currentThread().getName()); }); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } pool.shutdown(); } }
执行结果为:
我们代码中一共提交了5个任务(每个任务都有异常),从运行结果图中可以看出,线程池共创建出了5个线程来处理我们提交的任务,而没有复用之前创建的线程(之前的线程其实由于异常中断退出了)。所以如果向线程池中频繁的提交具有未捕获异常的任务,会降低线程池中线程的复用率,完全浪费了线程池本应具有的功效。如果主动的将异常捕获了,还会出现反复创建线程的问题吗?不会。
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolException_9 { public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.NANOSECONDS, new LinkedBlockingQueue<>(128)); for (int i = 0; i < 5; i++) { pool.execute(() -> { // 主动异常捕获 try { int a = 4 / 0; } catch (Exception e) { System.out.print("主动捕获异常 "); } System.out.println(Thread.currentThread().getName()); }); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } pool.shutdown(); } }
运行结果如下,使用的是用一个线程,由于异常被捕获了,前一个线程可以复用,无需创建新的线程。
主动的捕获异常没有什么问题,但是在实际的业务场景中我们不可能对所有提交的任务都主动捕获异常吧😂。那有没有既不使用try…catch,又能让线程进行复用呢?上面例子中可以发现使用的任务提交方法是execute方法,我们还有另一种任务提交方法submit。上面问题的解决办法就是用submit方法进行任务提交,即使线程任务中存在未捕获的运行时异常,线程仍可以复用。submit方法会将异常信息包装在Futrure实例内部,只有使用其get方法时才会将异常抛出。这样就不会对线程池中的线程执行造成影响。
import java.util.concurrent.*; public class ThreadPoolException_9 { public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.NANOSECONDS, new LinkedBlockingQueue<>(128)); for (int i = 0; i < 5; i++) { Future<?> future = pool.submit(() -> { System.out.println(Thread.currentThread().getName()); int a = 4 / 0; }); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 异常在get时才会抛出 try { System.out.println("获取任务执行结果"); future.get(); } catch (Exception e) { e.printStackTrace(); } } pool.shutdown(); } }
运行结果如下图,自始至终仅仅出现一个线程的名称。通过submit提交的任务,将异常抛出的时机延后,达到线程的复用。
Future怎么将线程中的异常进行保存和在get方法调用时才抛出可以看其实现类FutureTask类。(TODO 后面补充)
-
方式三:invokeAll和invokAny方法
ExecutorService.invokeAny()和ExecutorService.invokeAll()的使用剖析
7. 线程池的状态变化
线程池中有5中状态,这5中状态的转变如下图:
各个状态的含义及状态下能做的事情如下图:
说到状态可能就会想到线程的6种状态,那在线程池中「核心线程」和「非核心线程」在空闲时处于什么状态呢?
- ① 线程池中「核心线程」空闲时将处于WAITING状态
- ② 线程池中「非核心线程」空闲时处于TIMED_WAITING状态
可以通过实验进行验证,将会用到 jps
和jstack
命令查看JVM中的线程
-
①「核心线程」空闲时的状态测试
public class ThreadStateInThreadPool_7 { public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 5, 10, TimeUnit.MILLISECONDS, // 很短的线程空闲时间 new LinkedBlockingQueue<>(1)); for (int i = 0; i < 6; i++) { // 任务很快就执行完成 pool.execute(() -> System.out.println(Thread.currentThread().getName())); } Thread.sleep(100000000); pool.shutdown(); } }
我们的空闲线程存活时间设置的很短,所以当我们用
jps
查看虚拟机上的进程和使用jstack pid
生成线程快照,耗用的时间已经超过了10ms,此时线程池中仅存在空闲的「核心线程」,通过jstack pid
的输出我们可看到线程池中「核心线程」在空闲时的状态,及这个状态变化是产生的原因。>jps 16164 19316 ThreadStateInThreadPool_7 21284 Launcher 15340 Jps 15948 KotlinCompileDaemon > jstack 19316
可以看出「核心线程」处于WAITING状态,导致的原因是向阻塞队列中取任务进行执行时,返现队列中已经没有任务可以执行,就导致线程处于WAITING状态。 -
②「非核心线程」空闲时的状态测试
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadStateInThreadPool_8 { public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 5, 10, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1)); // 核心线程,使用了sleep很长时间,使核心线程长时间保持,不会变为非核心线程 pool.execute(()->{ System.out.println("任务1" + "使用的线程是:" + Thread.currentThread().getName()); try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } }); pool.execute(()->{ System.out.println("任务2" + "使用的线程是:" + Thread.currentThread().getName()); try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } }); // 非核心线程,由于线程池设置的空闲存活时间很长,所以也可以存在很长时间 pool.execute(()->System.out.println("任务3" + "使用的线程是:" + Thread.currentThread().getName())); for (int i = 0; i < 3; i++) { pool.execute(()-> System.out.println(Thread.currentThread().getName())); } Thread.sleep(100000000); pool.shutdown(); } }
上述运行结果为:
任务1使用的线程是:pool-1-thread-1 任务2使用的线程是:pool-1-thread-2 pool-1-thread-3 pool-1-thread-4 任务3使用的线程是:pool-1-thread-3 pool-1-thread-5
查看堆栈信息:
>jps 17216 KotlinCompileDaemon 16164 20504 Jps 9288 Launcher 3388 ThreadStateInThreadPool_8 > jstack 3388
从上图可看出「非核心线程」在空闲时,且在空闲存活时间到来之前,线程处于的状态TIMED_WAITING。可能会想为啥pool-1-thread-3,4,5就是非核心线程?可以按照下面这样分析:我们代码实现上写了两个sleep很长时间的任务,正好和我们设置的线程池核心线程数一致,另外还有4个很快就执行完成的任务,这样就可以让线程池创建出5个最大线程了(因为阻塞队列为1,所以弄了4个执行很快的任务),这样就确保了被sleep的任务就是对应核心线程,其余的线程就是非核心线程,非核心线程任务很快就执行完成进入空闲。
8. 线程池的关闭
在上面介绍了线程池的状态变化,我们再将图展示一遍:
线程池的状态值是保存在AtomicInteger对象
ctl
中高3位中:
ctl
变量的位含义被划分为2部分,理解ctl含义对看线程池实现代码很重要,如下图:
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; private static int ctlOf(int rs, int wc) { return rs | wc; }
所以线程池的状态从数值角度看,大小关系为:
RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED
线程池在使用完成之后,建议手动关闭,避免资源的占用。线程池中提供了两个关闭线程池的方法:shutdown()和shutdownNow()。这两个方法会修改线程池的状态分别为SHUTDOWN
和STOP
。
先来看一下这两个方法的使用:
import java.util.concurrent.*;
public class ThredPoolCloseDemo_10 {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5,
0, TimeUnit.NANOSECONDS, new LinkedBlockingQueue<>(2));
// 任务提交
// 线程池关闭
pool.shutdown();
// 或者调用
// pool.shutdownNow();
}
}
注意:这两个方法调用之后,线程池将不会接收新任务,如果仍有新任务提交,将会触发线程池对应的拒绝策略。
import java.util.concurrent.*; public class ThredPoolCloseDemo_12 { public static void main(String[] args) { // 线程池使用默认拒绝策略:AbortPolicy ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 0, TimeUnit.NANOSECONDS, new LinkedBlockingQueue<>(2)); // 关闭线程池 pool.shutdown(); // 线程池关闭之后再提交新任务将触发线程池的拒绝策略 pool.execute(()-> System.out.println("test")); } }
执行结果如下:
线程池调用完shutdown()之后会将线程池的状态设置为SHUTDOWN;调用完shutdownNow()方法之后将线程池的状态设置为STOP。在进一步看一下execute()方法的实现,就会明白为什么会触发拒绝策略:
上图中框出来的方法是execute方法内需要调用的方法,如果我们进一步看addWorker和isRunning方法实行逻辑,就可以看到其内部会对线程池的状态进行判断,在提交新任务时,如果线程池的状态大于等于SHUTDOWN,这两个方法将返回false。从而触发拒绝策略方法的调用。可自行看一下addWorker和isRunning方法内部实现,过一下流程,就明白为什么会触发拒绝策略了。
两者在使用上,看上去并没有什么差别,但这两个方法在实现逻辑和所达到的线程池关闭效果还是有区别的:
① 线程中断方面: 两个方法都涉及到线程的中断,首先中断线程时调用的都是线程提供的interrupt()
方法。不同在于是对线程池中的哪些线程调用其中断方法:shutdown()
方法仅仅对线程池中的空闲线程使用其interrupt()
方法;而shutdownNow()
方法对线程池中的所有线程(在处理任务的线程 + 空闲线程)都调用其interrupt()
方法。
注意:中断线程我们调用的是线程的
interrupt()
方法,该方法并不像Thread类中提供的已经过时的stop()
方法。interrupt()
方法仅仅是设置一下线程已经被中断的标志位true。具体可以搜一下Thread#interrupt()方法的讲解
关于怎么做到仅仅对空闲线程进行操作,或者说线程池是怎么来判断线程是空闲状态的?这个问题的核心在于ThreadPoolExecutor的一个内部类Worker类。Worker这个类继承自AbstractQueuedSynchronizer类(即AQS),并重写了相应方法,使得Worker类可以当做一个不可重入的锁来使用。而线程池中空闲状态就是通过Worker这个锁是否已经被占用。如果锁被占用(即调用尝试加锁方法失败),说明线程为非空闲线程,否则空闲。具体可以看Worker类中的run方法所调用的runWorker方法,这个方法是线程池中线程执行的核心方法,内部进行了加锁和解锁方法调用。
下图给出了当一个任务提交时(需要创建线程,不考虑太多的判断)所涉及到的方法和总体执行方向,希望对后续阅读原码有的大体的方向指引:图中对原码中很多细节都没有考虑进去,希望不要介意😂。
通过看getTask()方法你会发现线程池中空闲线程的存活时间的控制是使用阻塞队列中的具有超时退出的poll方法进行控制的。并不是通过什么定时任务来实现的。
② 线程池状态方面:shutdown()
方法调用后将线程池的状态设置为SHUTDOWN
状态;shutdownNow()
方法将线程池状态设置为STOP
状态。
③ 对任务的处理方面:
shutdown()
方法不会对正在运行的线程代用interrupt()
方法,即对运行中的线程无影响,详细的原因可以看原码中的ThreadPoolExecutor#interruptIdleWorkers()方法,可以看到里面有个w.tryLock()方法的调用。并且线程池会优先将线程池中阻塞队列中的任务执行完成之后再将空闲线程退出,即线程会使用空闲线程不断的去阻塞队列中获取任务进行执行(getTask()方法的实现逻辑)。只有将线程池中所有任务执行完毕后线程池才会关闭。shutdownNow()
方法,线程池中的所有线程都将调用interrupt()
方法。从getTask()方法实现逻辑上可以看出,由于此时线程池为STOP状态,所以阻塞队列中的任务都不会被执行,空闲的线程直接退出。对于正在执行任务的线程会继续将任务执行完成,如果执行的过程未对InterruptedException异常进行捕获处理,线程也将会退出。最终线程池会将阻塞队列中未执行的任务返回给shutdownNow()
方法的调用者。
注意:虽然说这两个方法时用来关闭线程池的方法,但是他们调用之后线程池并不会立即的关闭,从这两个方法的调用方视角,简单理解就是调用的shutdown()或shutdown()方法虽然执行完并返回了,但此时线程池中的线程可能还没有被全部关闭,而线程池需要等待相关工作都处理后才会关闭(如shutdown()方法要处理所有任务),所以说两个方法执行完成之后线程池不一定立即关闭。
可能又有疑问了,线程池没有被关闭,那shutdow()方法和shutdownNow()方法怎么会提前执行完呢?线程池还没有关闭,「关闭方法」就退出了,那「关闭方法」还有意义吗?回答这个问题,就要看这两个方法的实现逻辑了。
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 校验调用该方法的调用方是否有线程池的关闭权限 checkShutdownAccess(); // 将线程池的状态设置为SHUTDOWN advanceRunState(SHUTDOWN); // 对线程池中的空闲线程调用线程的interrupt()方法来中断空闲线程 interruptIdleWorkers(); // 可以看做一个占坑的方法 onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } // 尝试将线程池的状态设置为TERMINATED tryTerminate(); } public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 将线程池的状态设置为STOP advanceRunState(STOP); // 线程池中的所有线程都调用interrupt()方法 interruptWorkers(); // 取出阻塞队列中的任务 tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); return tasks; }
这两个方法中所调用的方法都可以说是一下状态设置方法,设置完状态就可以退出了,对于线程中线程的关闭和任务的处理就交给其他地方进行处理,如getTask()方法就会根据线程池的状态对线程进行退出操作。同时这两个方法还会给线程一个被中断的状态标志,所以这两个方法的意义可以认为是设置线程池状态的更新。
如果需要等待线程池关闭后,调用方代码再继续向下执行,则需要使用awaitTermination
方法来阻塞等待线程池的关闭(即循环判断线程池的状态是不是TERMINATED状态)。awaitTreminated
方法实现逻辑:
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 死循环判断线程池的状态是不是TERMINATED,同时有等待时间,
// 如果在等待时间内线程池的状态变为TERMINATED,则方法返回true,
// 如果在等待时间内线程池的状态仍不是TERMINATED则返回false
for (;;) {
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
if (nanos <= 0)
return false;
nanos = termination.awaitNanos(nanos);
}
} finally {
mainLock.unlock();
}
}
从上面的原码可以明白,awaitTermination
方法调用时指定的等待时间是一次性的,即该等待时间用完之后无法循环使用,所以等待时间一到,无论线程池是否已经达到TERMINATED状态,方法都会退出。所以awaitTermination
方法在使用时一般会和一个循环一起使用。如下使用样例:
import java.util.concurrent.*;
public class ThredPoolCloseDemo_11 {
private static final int WAIT_LOOP = 1000;
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5,
0, TimeUnit.NANOSECONDS, new LinkedBlockingQueue<>(2));
// 任务提交
// 线程池关闭
pool.shutdown();
// 等待线程池的关闭
if (!pool.isTerminated()) {
for (int i = 0; i < WAIT_LOOP; i++) {
try {
if (pool.awaitTermination(60, TimeUnit.SECONDS)) {
break;
}
// 60s还没关闭,将尝试关闭正在执行的线程,放弃没有执行的任务执行
pool.shutdownNow();
} catch (InterruptedException ignore) {
}
}
}
// 后续逻辑
}
}
如果想看更加详细的线程池关闭方法原码解析,感觉这篇文章写的不错:关闭线程池的正确姿势
9. 线程池空闲线程是怎么被回收的
上面在说线程池的关闭方法的时候,提到了空闲线程的关闭,除了这个之外,在7大参数「空闲线程存活时间」处也提到了空闲线程的关闭,那空闲线程的关闭具体情况是怎么样的呢?在这里将进行学习。。。。原码太难写了,我任务空闲线程的退出核心在于线程池中的getTask
这个方法中。
这个问题想看原码解析的可以看这篇文章:https://cloud.tencent.com/developer/news/758325
10. 线程池中的钩子方法
钩子方法,这是什么东西?从实现上看就是一个空实现的方法,其他方法在编写的时候先调用这个空方法,这样可以做到业务的扩展,怎么理解呢——子类仅仅重写这个钩子方法,不需要动其他方法的实现,就可以在父类功能上进行扩展,这就实现了业务功能的扩展。恩…钩子方法你就是个占着茅坑不拉屎的家伙。
说到占坑,就想到了设计模式中的模板模式。确实,钩子方法和模板模式确实有关系。下图展示出了钩子方法和模板模式之间的关系:
在此顺便对模板模式进行一下学习,模板模式:定义了一个业务的具体执行流程,即模板,而将流程中的每一阶段应该做的工作延迟到子类中做。即模板把要做的事情安排的明明白白。
在[4]这本书中有个例子,可以帮助理解模板模式的定义——就像西游记的99八十一难,基本每一关都是:师傅被掳走、打妖怪、妖怪被收走。具体什么妖怪你自己定义,怎么打你想办法,最后收走还是弄死看你本事,我只定义执行顺序和基本策略,具体的每一难由观音来安排。安排的清清楚楚😂。
模板模式定义了一个 具体业务(算法)执行流程,将这个定义流程的方法称为模板方法;一个流程会有多个执行阶段流程组成,而每一阶段对应的方法我们称为基本方法,基本方法负责业务流程中的具体逻辑,基本方法的形式就如上图列出的那些。
模板方法中会对基本方法进行调用,组成确定的执行流程,那么也就限制我们不可能用接口来实现这一需求,所以我们是通过抽象类的形式思想模板模式。同时为类防止子类对我们定义的模板进行改动,需要将模板方法用final修饰,下面通过一个简单的例子更好理解模板方法和基本方法之间的关系。
// 模板
public abstract class JourneyToTheWest {
// 模板方法
public final void chapter() {
// 每一难的故事情节
// 师傅没见了
masterLost();
// 打妖怪
fightMonster();
// 妖怪被处理
dealingWithMonster();
}
// 基本方法
// 师傅丢了
protected abstract void masterLost();
// 基本方法
// 打妖怪
protected void fightMonster() {};
// 基本方法
// 处理妖怪
protected void dealingWithMonster() {
System.out.println("妖怪被神仙收走");
}
}
// 第一难
public class ChapterOne extends JourneyToTheWest {
@Override
protected void masterLost() {
System.out.println("第一难:师傅你人呢?");
}
@Override
protected void fightMonster() {
System.out.println("妖怪吃我一棒");
}
}
// 第二难
public class ChapterTwo extends JourneyToTheWest {
@Override
protected void masterLost() {
System.out.println("第二难:师傅你人呢?");
}
@Override
protected void fightMonster() {
System.out.println("老虎吃我一棒");
}
@Override
protected void dealingWithMonster() {
System.out.println("妖怪挂了");
}
}
// 测试
public class JourneyToTheWestPlay {
public static void main(String[] args) {
ChapterOne chapterOne = new ChapterOne();
chapterOne.chapter();
System.out.println("第一章结束");
ChapterTwo chapterTwo = new ChapterTwo();
chapterTwo.chapter();
System.out.println("第二章结束");
}
}
// 输出:
第一难:师傅你人呢?
妖怪吃我一棒
妖怪被神仙收走
第一章结束
第二难:师傅你人呢?
老虎吃我一棒
妖怪挂了
第二章结束
有了上面对模板模式的理解,我们现在回到线程池中,线程池也有提供钩子方法,实现的作用和模板模式一样。线程池中提供了3个钩子方法:
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }
那这3个钩子方法在线程池的什么地方用到了呢?
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
// 省略逻辑
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// 省略逻辑
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 {
// 省略逻辑
w.unlock();
}
}
// 省略逻辑
} finally {
// 省略逻辑
}
}
runWorker
方法是执行线程池中任务的核心方法。beforeExecute
和afterExecute
这两个钩子方法都是在执行线程池中任务时调用的。并且这两个方法都是在同一个锁代码块内部进行执行的,和线程任务的执行使用的是同一个线程。beforeExecute
作为线程任务执行之前的钩子方法,可以开启一些线程任务的日志记录、开启信息统计、更新线程上下文信息等工作;afterExecute
作为线程任务执行之后的钩子方法,可以进行统计信息的收集、日志记录等工作。
如果我们重写的beforeExecute和afterExecute方法内部出现异常了会发生什么?
通过上面的runWorker原码,可以看出原码并没有对这两个方法中抛出的异常进行捕获处理,这就可能造成执行当前任务的线程退出终止,无法复用。
这个问题就是线程池中的线程在执行任务时,任务出现异常时线程会发生什么是同一个问题,这个我们上面做过实验——线程池内任务出现异常时线程将会怎么样
下面给出了使用beforeExecute
和afterExecute
这两个钩子方法进行任务总耗时统计的样例,代码中的ThreadLocal对象如果不了解就先将它看成一个线程间相互独立的局部变量。
public class HookFunction_14 {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5)) {
ThreadLocal<Long> startTime = new ThreadLocal<>();
// 重写其中的钩子方法
// 任务执行之前的钩子方法
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTime.set(System.currentTimeMillis());
System.out.println("当前线程为:" + t.getName() + ", 开始执行....");
super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("当前线程任务执行完成. 异常信息为:" + t);
System.out.println("任务中耗时为:" + (System.currentTimeMillis() - startTime.get()) + "ms");
}
};
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "线程任务正在执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
pool.shutdown();
}
}
// 执行结果如下,为了执行结果顺序能清晰反应方法执行顺序,所以上面代码就提交了一个任务
当前线程为:pool-1-thread-1, 开始执行....
pool-1-thread-1线程任务正在执行
当前线程任务执行完成. 异常信息为:null
任务中耗时为:1016ms
另外一个钩子方法时terminated
方法,调用它的原码如下:
final void tryTerminate() {
for (;;) {
// 省略逻辑
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated(); // 钩子方法
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
}
}
terminated
方法在线程池的状态变为TIDYING
之后调用,调用完该钩子方法之后线程池的状态将被设置为TERMINATED
状态。其使用方式和前两个钩子方法一样,也是在子类中进行重写,实现具体的逻辑,所以就不写案例了(其实是不知道具体的应用场景是什么,没有用过😂,希望大佬给点指导)。
另外有一点我们可能已经注意到,
runWorker
和tryTerminate
方法其都是final方法,这也是模板模式在实现上为了保证模板方法不被子类重写特意而为。
11. 线程池中涉及的设计模式整理
已经知道的几个比较简单的设计模式有:工厂模式、模板模式。
模板模式上面已经说过了,就不在写了。
下面学习一下工厂模式。工厂模式可以细分成如下三个:
简单工厂模式,其实是工厂方法模式的特例——工厂方法为静态方法。
哎,不知道怎么清楚的表达,原因是自己理解的不深刻,就不写了。写错了就不好了😂。
参考
[1] Java 8与Runtime.getRuntime().availableProcessors()
[2] https://blog.csdn.net/MingHuang2017/article/details/79571529
[3] https://blog.csdn.net/weixin_42227763/article/details/119514959
[4] 《重学 Java 设计模式》小傅哥
[5] 《Head First设计模式》