文章目录
一、线程:
在触及线程池之前,先简单捋一捋线程相关的知识点。
线程是操作系统能够进行运算调度的最小单位。
①线程的创建
●继承Thread类。
public class CreatingThread extends Thread {
public static void main(String[] args) {
new CreatingThread().start();
new CreatingThread().start();
new CreatingThread().start();
}
@Override
public void run() {
System.out.println(getName() + " is running");
}
}
由于一个类只能继承一个父类,如果这个类本身已经继承了其它类,就不能使用这种方式了。
●实现Runnable接口。
public class CreatingThread implements Runnable {
public static void main(String[] args) {
new Thread(new CreatingThread()).start();
new Thread(new CreatingThread()).start();
new Thread(new CreatingThread()).start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
由于一个类可以实现多个接口,因此好处是不会影响其继承体系。
●匿名内部类。
public class CreatingThread {
public static void main(String[] args) {
new Thread(()-> System.out.println(Thread.currentThread().getName() + " is running")).start();
}
}
可以使用传统写法,推荐使用JDK8提供的lambda方式。
●实现Callable接口
public class CreatingThread implements Callable<Long> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Long> task = new FutureTask<>(new CreatingThread());
new Thread(task).start();
System.out.println("等待完成任务");
Long result = task.get();
System.out.println("任务完成id:" + result);
}
@Override
public Long call() throws Exception {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " is running, id is " + Thread.currentThread().getId());
return Thread.currentThread().getId();
}
}
可以获取线程执行的结果。(FutureTask实现了Runnable接口)
●定时器
public class CreatingThread {
public static void main(String[] args) {
Timer timer = new Timer();
// 每秒执行一次run()方法
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}, 0 , 1000);
}
}
定时任务。(TimerTask也实现了Runnable接口)
●线程池
public class CreatingThread {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 20; i++) {
threadPool.execute(()-> System.out.println(Thread.currentThread().getName() + " is running"));
}
}
}
使用线程池可以复用线程,节约系统资源。
●并行Stream
public class CreatingThread {
public static void main(String[] args) {
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);
// 串行,打印结果为12345
list.forEach(System.out::print);
System.out.println();
// 并行,打印结果随机
list.parallelStream().forEach(System.out::print);
}
}
JDK8中流相关知识。
●Spring的异步方法
启动类加注解@EnableAsync
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
方法加注解@Async
@Service
public class CreatingThreadService {
@Async
public void call() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
使用方法
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class CreatingThreadTest {
@Autowired
private CreatingThreadService creatingThreadService;
@Test
public void test() {
creatingThreadService.call();
creatingThreadService.call();
creatingThreadService.call();
}
}
可以在Spring和Springboot中使用,为了方便使用Springboot举例。
适用于前后逻辑不相关联的需要异步调用的方法,比如大量发送短信等功能。
上面虽然介绍了很多方法,但其实有应用与取巧的部分,本质上还是两类方法:继承Thread类和实现Runnable接口。
如果同时继承了Thread类且实现了Runnable接口,会怎样执行?
正常情况下我们不会这么干,但是本着研究的精神和异常排查,我们稍微再深入一些。这个问题的答案就在Thread类的源码中:
public
class Thread implements Runnable {
// Thread中维护了一个Runnable实例
private Runnable target;
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
......
// 构造方法传进来的Runnable赋值给target
this.target = target;
......
}
@Override
public void run() {
// 如果target不为空,会执行target的run()方法
if (target != null) {
target.run();
}
}
}
举个例子:
public class CreatingThread {
public static void main(String[] args) {
new Thread(() -> System.out.println("Runnable: " + Thread.currentThread().getName())) {
@Override
public void run() {
System.out.println("Thread: " + getName());
}
}.start();
}
}
执行结果为:
Thread: Thread-x
②线程状态
线程的状态其实就定义在线程类的内部,以枚举的方式定义:
public enum State {
// 新建
NEW,
// 就绪
RUNNABLE,
// 阻塞
BLOCKED,
// 等待
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
详细说明如下:
③线程状态的转换
线程创建之后调用start()方法开始运行,当调用wait()、join()、LockSupport.lock()方法后,线程会进入到WAITING状态。同样的wait(long)、sleep(long)、join(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil(long)在此基础上增加了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING状态,当超时等待时间到达后,线程会切换到RUNNABLE状态。
当线程处于WAITING和TIMED _WAITING状态时,可以通过Object.notify()、Object.notifyAll()方法使线程转换到RUNNABLE状态。
当线程出现资源竞争时,在等待获取锁的时候,线程会进入到BLOCKED阻塞状态,当线程获取锁时,线程进入到RUNNABLE状态。
线程运行结束后,进入到TERMINATED状态。这也是线程的生命周期。
这里还有一个细节需要注意:当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态;而当线程使用Lock进行加锁的时候,线程切换到的是WAITING或者TIMED_WAITING状态,因为Lock会调用LockSupport的方法。
线程状态的转换操作
1、interrupt
中断可以理解为线程的一个标志位,它标志了一个运行中的线程是否被其他线程进行了中断操作。当调用interrupt()方法抛出异常时,会清除中断标志位。
针对标志位查询,提供了isInterrupted()方法可以返回线程是否被中断。如果使用interrupted()方法查询,查询完后会清除中断标志位。
中断操作可以看做线程间一种简便的交互方式。一般在结束线程时通过中断标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。
2、join
join可以看做是线程间协作的一种方式。如果一个线程的输入非常依赖于另一个线程的输出,但执行时另一个线程落在了后面,这个线程就要等另一个线程追上自己。
如果threadA执行了threadB.join(),其含义是:threadA会等待threadB终止后才继续执行。
3、sleep
sleep是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。
需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。.
sleep() VS wait()
●sleep()方法是Thread的静态方法,而wait是Object的实例方法。
●wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁;而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源;而sleep()方法只是会让出CPU并不会释放掉对象锁。
●sleep()方法在休眠时间达到后,如果再次获得CPU时间片就会继续执行;而wait()方法必须等待Object.notify()/Object.notifyAll()通知后,才会离开等待池,如果再次获得CPU时间片才会继续执行。
4、yield
yield执行后,会使当前线程让出CPU。需要注意的是:让出CPU并不代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片,当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。
在Java中通过一个整型成员变量priority来控制优先级,优先级的范围从1到10。在构建线程的时候可以通过setPriority()方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程概率上优先获得处理器时间片。需要注意的是:在不同JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。
yield() VS sleep()
●二者都会使当前线程让出处理器资源。
●sleep()让出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。
●yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。
④守护线程
守护线程(Daemon)也叫精灵线程,是一种特殊的线程,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。
用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。
需要注意的是:守护线程在退出的时候并不会执行finally块中的代码,所以将释放资源等操作不要放在finally块中执行,这种操作是不安全的。
线程可以通过setDaemon(true)方法将线程设置为守护线程。需要注意的是:设置守护线程要先于start()方法,否则会报错,作为用户线程执行。
二、线程池:
在实际使用中,线程是很占用系统资源的,如果对线程管理不善很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有以下好处:
●降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗。
●提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度。
●提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。
①体系结构
如图所示,是一些比较重要的接口和类。
1、Executor
线程池顶级接口,只定义了一个执行无返回值任务的方法。
public interface Executor {
// 执行无返回值任务
void execute(Runnable command);
}
2、ExecutorService
线程池次级接口,对Executor做了一些扩展,主要增加了关闭线程池、执行有返回值任务、批量执行任务的方法。
public interface ExecutorService extends Executor {
// 关闭线程池(不再接受新任务,但已经提交的任务会执行完成)
void shutdown();
// 立即关闭线程池(尝试停止正在运行的任务,未执行的任务将不再执行,被迫停止及未执行的任务将以列表的形式返回)
List<Runnable> shutdownNow();
// 检查线程池是否已关闭
boolean isShutdown();
// 检查线程池是否已终止(只有在shutdown()或shutdownNow()之后调用才有可能为true)
boolean isTerminated();
// 检查线程池是否到点已终止(只有在指定时间内线程池达到终止状态了才会返回true)
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
// 执行有返回值的任务,任务的返回值为task.call()的结果
<T> Future<T> submit(Callable<T> task);
// 执行有返回值的任务,任务的返回值为这里传入的result(只有当任务执行完成了调用get()时才会返回)
<T> Future<T> submit(Runnable task, T result);
// 执行有返回值的任务,任务的返回值为null(只有当任务执行完成了调用get()时才会返回)
Future<?> submit(Runnable task);
// 批量执行任务,只有当这些任务都完成了这个方法才会返回
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
// 在指定时间内批量执行任务,未执行完成的任务将被取消(timeout是所有任务的总时间,不是单个任务的时间)
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 返回任意一个已完成任务的执行结果,未执行完成的任务将被取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
// 在指定时间内如果有任务已完成,则返回任意一个已完成任务的执行结果,未执行完成的任务将被取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
3、AbstractExecutorService
抽象类,运用模板方法实现了部分方法,主要为执行有返回值任务、批量执行任务的方法。将任务包装成了FutureTask。
public abstract class AbstractExecutorService implements ExecutorService {
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
......
}
4、ThreadPoolExecutor
普通线程池类,也是我们通常所说的线程池,包含最基本的一些线程池操作相关的方法实现,比如线程的创建、任务的处理、拒绝策略等。
public class ThreadPoolExecutor extends AbstractExecutorService {
......
}
5、ForkJoinPool
JDK7中新增的线程池类,与Go中的线程模型特别类似,都是基于工作窃取理论,特别适合于处理归并排序这种先分后合的场景。
@sun.misc.Contended
public class ForkJoinPool extends AbstractExecutorService {
......
}
6、Executors
线程池工具类,定义了一系列快速实现线程池的方法。阿里手册中不建议使用这个类来新建线程池。但是在把握其利弊的情况下,也有其用武之地。
public class Executors {
......
}
7、ScheduledExecutorService
对ExecutorService做了一些扩展,增加了一些定时任务相关的功能,主要包含两大类:执行一次、重复多次执行。
public interface ScheduledExecutorService extends ExecutorService {
// 在指定延时后执行一次
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
// 在指定延时后执行一次
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
// 在指定延时后开始执行,并在之后以指定时间间隔重复执行(间隔不包含任务执行的时间)。相当于之后的延时以任务开始计算
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
// 在指定延时后开始执行,并在之后以指定延时重复执行(间隔包含任务执行的时间)。相当于之后的延时以任务结束计算
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
8、ScheduledThreadPoolExecutor
定时任务线程池类,用于实现定时任务相关功能:将任务包装成定时任务,并按照定时策略来执行。
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
......
}
使用延时队列实现,但并没有直接使用DelayQueue,而是自己又实现了一个DelayedWorkQueue,不过二者的实现原理是一样的。DelayedWorkQueue中使用堆实现功能。
②线程池状态
上面线程部分介绍了线程状态,对于线程池来说也有自己的状态,但是不像线程状态一样一目了然,下面来一起看一看:
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;
private static final int RUNNING = -1 << COUNT_BITS; //111 00000...
private static final int SHUTDOWN = 0 << COUNT_BITS; //000 00000...
private static final int STOP = 1 << COUNT_BITS; //001 00000...
private static final int TIDYING = 2 << COUNT_BITS; //010 00000...
private static final int TERMINATED = 3 << COUNT_BITS; //011 00000...
// 线程池的状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 线程池中工作线程的数量
private static int workerCountOf(int c) { return c & CAPACITY; }
// 计算ctl的值,等于运行状态“加上”线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; }
对于这些变量,说明如下:
●线程池的状态和工作线程的数量共同保存在控制变量ctl中,类似于AQS中的state变量。不过这里是直接使用的AtomicInteger,这里换成unsafe+volatile也是可以的。
●ctl的高3位保存运行状态。低29位保存工作线程的数量,也就是说线程的数量最多只能有(2^29-1)个,也就是上面的CAPACITY。
●线程池的状态一共有五种,分别是RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。
●RUNNING,表示可接受新任务,且可执行队列中的任务。
●SHUTDOWN,表示不接受新任务,但可执行队列中的任务。
●STOP,表示不接受新任务,且不再执行队列中的任务,且中断正在执行的任务。
●TIDYING,表示所有任务已经中止且工作线程数量为0,最后变迁到这个状态的线程将要执行terminated()钩子方法,只会有一个线程执行这个方法。
●TERMINATED,表示中止状态,已经执行完terminated()钩子方法。
③线程池状态的转换
④线程池的创建
JDK 封装的方法
JDK 封装、提供了创建线程池的 4 个方法:
●FixedThreadPool:固定线程池。该方法返回一个固定线程数量的线程池(该线程池中的线程数量始终不变)。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行,若没有,则新的任务会被暂存在一个任务队列(默认无界队列)中,待有线程空闲时,便处理在任务队列中的任务。
ExecutorService service = Executors.newFixedThreadPool(5);
●SingleThreadPool:单例线程池。该方法返回一个只有一个线程的线程池。若多于一个任务被提交到该线程池,任务会被保存在一个任务队列(默认无界队列)中,待线程空闲,按先入先出的顺序执行队列中的任务。
ExecutorService service = Executors.newSingleThreadExecutor();
●CachedThreadPool:缓存线程池。该方法返回一个可根据实际情况调整线程数量的线程池,线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。如果所有可复用线程均在工作,此时有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
ExecutorService service = Executors.newCachedThreadPool();
●ScheduledThreadPool:任务调用线程池。该方法也返回一个 ScheduledThreadPoolExecutor 对象,该线程池可以指定线程数量。
ExecutorService service = Executors.newScheduledThreadPool(9);
注意:由于这些方法高度封装,因此,如果使用不当,出了问题将无从排查。因此,建议程序员自己手动创建线程池,而手动创建的前提就是了解线程池的参数设置。下面就来看看如何手动创建线程池。
正确的创建姿势
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
这里可以看到有7个入参,都有比较重要的作用:
●corePoolSize:核心线程池的大小。当提交一个任务时,如果当前线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务(即使当前核心线程池有空闲的线程)。如果当前线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候所有的核心线程都会被创建并且启动。
●maximumPoolSize:线程池能创建线程的最大个数。
●keepAliveTime:空闲线程存活时间。
●unit:时间单位。keepAliveTime的时间单位。
●workQueue:阻塞队列。
●threadFactory:创建线程的工厂类。
●handler:线程池的拒绝策略。
拒绝策略
JDK拒绝策略
在分析 JDK 自带的线程池拒绝策略前,先看下 JDK 定义的 拒绝策略接口,如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
接口定义很明确,当触发拒绝策略时,线程池会调用外部设置的具体的策略,将当前提交的任务以及线程池实例本身传递给外部处理,具体作何处理,不同场景会有不同的考虑。
●AbortPolicy:直接拒绝所提交的任务,并抛出 RejectedExecutionException 异常。
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
注意要正确处理抛出的异常。(ThreadPoolExecutor 中默认的策略就是 AbortPolicy)
●CallerRunsPolicy:只用调用者所在的线程来执行任务。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。
●DiscardPolicy:不处理直接丢弃掉任务。
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
如果提交的任务无关紧要,就可以使用它 。因为它就是个空实现,会悄无声息的吞噬失败的任务。
●DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。
第三方实现的拒绝策略
●dubbo中的线程拒绝策略
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
private final String threadName;
private final URL url;
private static volatile long lastPrintTime = 0;
private static Semaphore guard = new Semaphore(1);
public AbortPolicyWithReport(String threadName, URL url) {
this.threadName = threadName;
this.url = url;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED!" +
" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg);
dumpJStack();
throw new RejectedExecutionException(msg);
}
private void dumpJStack() {
//省略实现
}
}
可以看到,当 dubbo 的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因:
●输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用 dubbo 的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有 spring。得益于这么详细的日志,可以很容易定位到问题所在。
●输出当前线程堆栈详情,这个太有用了,当通过上面的日志信息还不能定位问题时,案发现场的 dump 线程上下文信息就是发现问题的救命稻草。
●继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性。
●Netty中的线程池拒绝策略
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
NewThreadRunsPolicy() {
super();
}
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
final Thread t = new Thread(r, "Temporary task executor");
t.start();
} catch (Throwable e) {
throw new RejectedExecutionException(
"Failed to start a new thread", e);
}
}
}
Netty 中的实现很像 JDK 中的 CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy 是直接在调用者线程执行的任务。而 Netty 是新建了一个线程来处理的。
所以,Netty 的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty 的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到 new 不出新的线程了,才会抛创建线程失败的异常。
●activeMQ中的线程池拒绝策略
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
try {
executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
}
throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
}
});
activeMQ中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常。
●pinpoint中的线程池拒绝策略
public class RejectedExecutionHandlerChain implements RejectedExecutionHandler {
private final RejectedExecutionHandler[] handlerChain;
public static RejectedExecutionHandler build(List<RejectedExecutionHandler> chain) {
Objects.requireNonNull(chain, "handlerChain must not be null");
RejectedExecutionHandler[] handlerChain = chain.toArray(new RejectedExecutionHandler[0]);
return new RejectedExecutionHandlerChain(handlerChain);
}
private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) {
this.handlerChain = Objects.requireNonNull(handlerChain, "handlerChain must not be null");
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
for (RejectedExecutionHandler rejectedExecutionHandler : handlerChain) {
rejectedExecutionHandler.rejectedExecution(r, executor);
}
}
}
pinpoint 的拒绝策略实现很有特点,和其他的实现都不同。它定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的 rejectedExecution 依次执行一遍。
如何设计线程池中的线程数量
线程池的大小对系统的性能有一定的影响,过大或者过小的线程数量都无法发挥最优的系统性能。但是线程池大小的确定也不需要做的非常精确,因为只要避免极大和极小两种情况,线程池的大小对性能的影响都不会影响太大。一般来说,确定线程池的大小需要考虑CPU数量,内存大小等因素。
在《Java Concurrency in Practice》书中给出了一个估算线程池大小的经验公式:
公式还是比较复杂的,简单来说,就是如果是CPU密集型运算,那么线程数量和CPU核心数相同就好,避免了大量无用的切换线程上下文,如果是IO密集型的话,需要大量等待,那么线程数可以设置的多一些,比如CPU核心乘以2。
至于如何获取 CPU 核心数,Java 提供了一个方法:
// 返回CPU的核心数量
Runtime.getRuntime().availableProcessors();
在实际应用中,线程等待时间所占比例越高,会需要越多线程;线程CPU时间所占比例越高,需要越少线程。这就可以划分成两种任务类型:
●IO密集型,2Ncpu(可以测试后自己控制大小,2Ncpu一般没问题)(常出现于在线程中执行:数据库数据交互、文件上传下载、网络数据传输等等)
●计算密集型,Ncpu(常出现于线程中:复杂算法)
⑤线程池的关闭
关闭线程池,可以通过shutdown()和shutdownNow()这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。但二者还是有不一样的地方:
●shutdown()只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
●shutdownNow()首先将线程池的状态设置为STOP状态,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
可以看出shutdown()方法会将正在执行的任务继续执行完,而shutdownNow()会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown()方法都会返回true;当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated()方法才会返回true。
⑥普通任务的执行
1、execute()方法
先来看线程池提交任务的方法,也是核心方法。(任务提交后不一定立即执行)
public void execute(Runnable command) {
// 任务不能为空
if (command == null)
throw new NullPointerException();
// 获取控制变量ctl
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();
// 再次检查线程池状态,如果不是运行状态,就移除任务并执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 判断容错检查工作线程数量是否为0,如果为0就创建一个
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果任务入队列失败,尝试创建非核心工作线程
else if (!addWorker(command, false))
// 非核心工作线程创建失败,执行拒绝策略
reject(command);
}
提交任务的过程大致如下:
●如果工作线程数量小于预定核心数量,直接创建核心线程。
●如果达到预定核心数量,进入任务队列。
●如果任务队列满了,尝试创建非核心线程。
●如果线程数达到最大数量,执行拒绝策略。
2、Worker内部类
Worker内部类可以看作是对工作线程的包装。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
// 真正工作的线程
final Thread thread;
// 第一个任务(从构造方法传入)
Runnable firstTask;
// 完成任务数
volatile long completedTasks;
// 构造方法
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 使用线程工厂生成一个线程,把Worker本身作为Runnable传给线程
this.thread = getThreadFactory().newThread(this);
}
// 实现Runnable的run()方法
public void run() {
// 调用ThreadPoolExecutor中的runWorker()方法
runWorker(this);
}
// 锁方法
......
}
3、addWorker()方法
方法的主要目的是创建一个工作线程并启动,其间会做线程池状态、工作线程数量等各种检测。
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 线程池状态检查
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
}
}
// 执行到此,工作线程数量已加1
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;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
// 再次检查线程池的状态
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 {
// 线程启动失败,执行失败方法(线程数量减1,执行tryTerminate()方法等)
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
4、runWorker()方法
此方法是真正执行任务的方法。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// 强制释放锁(shutdown()里面加的锁),无视中断标记
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 取任务,如果有第一个任务,就先执行第一个任务。然后从任务队列中取任务执行(能取到任务就不停循环)
while (task != null || (task = getTask()) != null) {
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
// 检查线程池的状态
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置为空,重新从队列中取
task = null;
// 完成任务数加1
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
5、getTask()方法
此方法从队列中获取任务,里面包含了对线程池状态、空闲时间等各种检测。
private Runnable getTask() {
// 是否超时
boolean timedOut = false; // Did the last poll() time out?
// 自旋
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 线程池状态是SHUTDOWN的时候会把队列中的任务执行完直到队列为空,线程池状态是STOP时立即退出
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
// 工作线程数量
int wc = workerCountOf(c);
// Are workers subject to culling?
// 是否允许超时,有两种情况:
// 一种是允许核心线程超时,也就是说所有的线程都可能超时。
// 另一种是工作线程数大于了预定核心线程数,这种肯定是允许超时的。
// 其实非核心线程是一定允许超时的(这里的超时其实是指取任务超时)
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 超时判断(还包含一些容错判断)
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 超时了,减少工作线程数量,并返回null
if (compareAndDecrementWorkerCount(c))
return null;
// 减少工作线程数量失败,则重试
continue;
}
// 真正取任务的地方
try {
// 默认情况下,只有当工作线程数量大于核心线程数量时,才会调用poll()方法触发超时调用
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 取到任务了就正常返回
if (r != null)
return r;
// 没取到任务表明超时了(回到continue的if后返回null)
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
取任务时会根据工作线程的数量判断是使用BlockingQueue的poll()方法还是take()方法。
●poll()方法会在超时时返回null,如果timeout<=0,队列为空时直接返回null。
●take()方法会一直阻塞直到取到任务或抛出中断异常。
所以,如果keepAliveTime设置为0,当任务队列为空时,非核心线程取不出来任务,会立即结束其生命周期。
默认情况下,是不允许核心线程超时的,但是可以通过allowCoreThreadTimeOut()方法设置,使核心线程也可超时。
6、总结
●execute()方法:提交任务的方法,根据核心线程数、任务队列大小、最大线程数,分成四种拒绝策略判断任务应该往哪去。
●addWorker()方法:添加工作线程的方法,通过Worker内部类封装一个Thread实例维护工作线程的执行。
●runWorker()方法:真正执行任务的方法,先执行第一个任务,再不断从任务队列中取任务来执行。
●getTask()方法:真正从队列取任务的地方,默认情况下,根据工作线程数量与核心线程数量的关系判断使用poll()还是take()方法,keepAliveTime参数也是在这里使用的。
核心线程 vs 非核心线程:
二者主要是根据corePoolSize来判断任务该去哪里,两者在执行任务的过程中并没有任何区别。有可能新建的时候是核心线程,而keepAliveTime时间到了结束了的也可能是刚开始创建的核心线程。
⑦未来任务的执行
在Executors框架体系中,FutureTask用来表示可获取结果的异步任务,FutureTask实现了Future接口。
FutureTask提供了启动和取消异步任务,查询异步任务是否计算结束以及获取最终的异步任务的结果的一些常用的方法。通过get()方法来获取异步任务的结果,但是会阻塞当前线程直至异步任务执行结束。一旦任务执行结束,任务不能重新启动或取消,除非调用runAndReset()方法。
1、FutureTask的状态
FutureTask内部定义了一些自己的状态,后续的操作会使用这些状态。
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
2、AbstractExecutorService中的方法
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
// 将普通任务包装成FutureTask
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
// 将普通任务包装成FutureTask
return new FutureTask<T>(callable);
}
public Future<?> submit(Runnable task) {
// 非空判断
if (task == null) throw new NullPointerException();
// 将普通任务包装成FutureTask
RunnableFuture<Void> ftask = newTaskFor(task, null);
// 交给execute()方法去执行
execute(ftask);
// 返回FutureTask
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
这里定义了一些模板方法。
●newTaskFor()方法用于将普通任务包装成FutureTask。
●submit()方法内部调用newTaskFor()方法,然后交给execute()方法去执行,最后返回FutureTask本身。
3、run()方法
同上,execute()方法最后调用的是task的run()方法,用于真正执行任务。
public void run() {
// 状态不为NEW,或者修改为当前线程来运行这个任务失败,则直接返回
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
// 真正的任务
Callable<V> c = callable;
// 判断任务非空,再次判断状态(状态必须为NEW时才运行)
if (c != null && state == NEW) {
// 运行的结果
V result;
boolean ran;
try {
// 任务执行的地方
result = c.call();
// 任务执行成功
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 处理异常
setException(ex);
}
if (ran)
// 处理结果
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
// runner置空
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
// 处理中断
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
protected void setException(Throwable t) {
// 将状态从NEW变为COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 返回值设为传进来的异常
outcome = t;
// 最终的状态变为EXCEPTIONAL
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
protected void set(V v) {
// 将状态从NEW变为COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 返回值设为传进来的结果
outcome = v;
// 最终的状态变为NORMAL
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
private void finishCompletion() {
// assert state > COMPLETING;
// 判断队列不为空(这个队列实际上为调用者线程)
for (WaitNode q; (q = waiters) != null;) {
// 队列置空
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
// 调用者线程
Thread t = q.thread;
if (t != null) {
q.thread = null;
// 如果调用者线程不为空,则唤醒它
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
// 钩子方法,可被子类重写
done();
// 任务置空
callable = null; // to reduce footprint
}
代码整体流程如下:
●FutureTask有一个状态state控制任务的运行过程,正常运行结束state变化为NEW->COMPLETING->NORMAL,异常运行结束state变化为NEW->COMPLETING->EXCEPTIONAL。
●FutureTask保存了运行任务的线程runner,它是线程池中的某个线程。
●调用者线程是保存在waiters队列中的。(逻辑在get()方法中)
●任务执行完毕,除了设置状态state之外,还会唤醒调用者线程。
4、get()方法
public V get() throws InterruptedException, ExecutionException {
int s = state;
// 如果状态小于等于COMPLETING,则进入队列等待
if (s <= COMPLETING)
s = awaitDone(false, 0L);
// 返回结果或异常
return report(s);
}
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// 获取超时时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
// 自旋
for (;;) {
// 处理中断
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
// 自旋出口
// 如果状态大于COMPLETING,则跳出循环并返回
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
// 如果状态等于COMPLETING,说明任务快完成了,就差设置状态(到NORMAL或EXCEPTIONAL)和设置结果了。让出CPU,优先完成任务
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
// 如果队列为空,初始化队列(WaitNode中记录了调用者线程)
else if (q == null)
q = new WaitNode();
// 如果未进入队列,尝试入队
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
// 超时处理
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
// 阻塞当前(调用者)线程
LockSupport.park(this);
}
}
private V report(int s) throws ExecutionException {
Object x = outcome;
// 任务正常结束
if (s == NORMAL)
return (V)x;
// 任务被取消了
if (s >= CANCELLED)
throw new CancellationException();
// 执行异常
throw new ExecutionException((Throwable)x);
}
代码逻辑主要在自旋的地方,假设状态为NEW进入,整体流程如下:
●第一次循环,状态为NEW,队列为空,初始化队列并把调用者线程封装在WaitNode中。
●第二次循环,状态为NEW,队列不为空,让包含调用者线程的WaitNode入队。
●第三次循环,状态为NEW,队列不为空且已入队,阻塞调用者线程。
●任务执行完毕了,在run()方法的最后会unpark()方法调用者线程,也就是3处会被唤醒。
●第四次循环,状态大于COMPLETING了,退出循环并返回。
●如果正常执行结束,则返回任务的返回值。
●如果异常结束,则包装成ExecutionException异常抛出。
5、cancel()方法
public boolean cancel(boolean mayInterruptIfRunning) {
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
if (mayInterruptIfRunning) {
try {
Thread t = runner;
if (t != null)
t.interrupt();
} finally { // final state
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
finishCompletion();
}
return true;
}
6、总结
●未来任务是通过把普通Task包装成FutureTask来实现的。
●通过FutureTask不仅能够获取任务执行的结果,还有感知到任务执行的异常,甚至还可以取消任务。
●AbstractExecutorService中定义了一些模板方法,在FutureTask中会使用到。这种设计思想值得学习。
●FutureTask是典型的异步调用的实现方式,这种设计思想在Netty、Dubbo等框架中也很常见。
关于状态的更多理解
在《Java并发编程的艺术》中,将FutureTask的状态分为三类:
●未启动。对应状态NEW。run()方法执行之前,FutureTask处于未启动状态。
●已启动。对应状态COMPLETING。run()方法执行的过程中,FutureTask处于已启动状态。
●已完成。对应状态NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTING、INTERRUPTED。run()方法执行结束,或者调用cancel()方法取消任务,或者在执行任务期间抛出异常,这些情况都称之为FutureTask的已完成状态。
对于整体而言,状态的流转为:未启动 —> 已启动 —> 已完成。
当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞;当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或抛出异常。
当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将导致此任务永远不会被执行;当FutureTask处于已启动状态时,执行FutureTask.cancel(true)方法将以中断执行此任务线程的方式来试图停止任务;当FutureTask处于已启动状态时,执行FutureTask.cancel(false)方法将不会对正在执行此任务的线程产生影响(让正在执行的任务运行完成);当FutureTask处于已完成状态时,执行FutureTask.cancel()方法将返回false。
RPC框架中的应用
RPC框架常用的调用方式有同步调用、异步调用,其实本质上都是异步调用,正是用FutureTask的方式来实现的。
一般地,通过一个线程(远程线程)去调用远程接口,如果是同步调用,则直接让调用者线程阻塞着等待远程线程调用的结果,待有结果返回了再返回;如果是异步调用,则先返回一个未来可以获取到远程结果的东西FutureXxx,当然,如果这个FutureXxx在远程结果返回之前调用了get()方法一样会阻塞着调用者线程。
⑧定时任务的执行
ScheduledThreadPoolExecutor可以用在定时执行任务或者周期性执行任务场景中,相对于任务调度的Timer来说,其功能更加强大。Timer只能使用一个后台线程执行任务,而ScheduledThreadPoolExecutor则可以通过构造函数来指定后台线程的个数。
ScheduledThreadPoolExecutor有两个重要的内部类:DelayedWorkQueue和ScheduledFutureTask。DelayedWorkQueue 实现了BlockingQueue接口,也就是一个阻塞队列。ScheduledFutureTask 则是继承了 FutureTask类,表示该类用于返回异步任务的结果。
定时任务总体分为四种:
●未来执行一次的任务,无返回值。
●未来执行一次的任务,有返回值。
●未来按固定频率重复执行的任务。
●未来按固定延时重复执行的任务。
1、构造方法
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), handler);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
由于ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,它的构造方法实际上是调用了ThreadPoolExecutor的构造方法。线程池允许最大的线程个数为Integer.MAX_VALUE。
2、ScheduledExecutorService中的方法
ScheduledExecutorService接口定义了可延时执行异步任务和可周期执行异步任务的特有功能。
// 达到给定的延时时间后,执行任务(无结果)
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
// 达到给定的延时时间后,执行任务(有结果)
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
// 从上一个任务开始的时间计时,period时间后,检测上一个任务是否执行完毕
// 如果上一个任务执行完毕,则当前任务立即执行;如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后执行
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
// 当达到延时时间initialDelay后,任务开始执行
// 上一个任务执行结束后到下一次任务执行,中间延时时间间隔为delay
// 周期性执行任务
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
3、ScheduledFutureTask
ScheduledThreadPoolExecutor最大的特色是能够周期性执行异步任务,当调用schedule()、scheduleAtFixedRate()和scheduleWithFixedDelay()方法时,实际上是将提交的任务转换成ScheduledFutureTask类。
ⅰ、schedule()
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
// 参数判断
if (command == null || unit == null)
throw new NullPointerException();
// 将传入的Runnable类装饰成ScheduledFutureTask类
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
// 延时执行
delayedExecute(t);
return t;
}
通过decorateTask()方法会将传入的Runnable类装饰成ScheduledFutureTask类,然后交给了delayedExecute()方法:
private void delayedExecute(RunnableScheduledFuture<?> task) {
// 如果线程池关闭了,执行拒绝策略
if (isShutdown())
reject(task);
else {
// 先把任务加入任务队列
super.getQueue().add(task);
// 再次检查线程池状态
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
// 保证工作线程足够
ensurePrestart();
}
}
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
// 创建工作线程
// 这里没有传入firstTask参数,因为上面已经把任务加入任务队列了
// 另外,这里没有使用maxPoolSize参数,所以最大线程数量在定时线程池中实际是没有用的
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
可以看到,实际上这里只是控制任务能不能被执行,真正执行任务的地方在任务的run()方法中。
为了保证ScheduledThreadPoolExecutor能够延时执行任务以及周期性执行任务,ScheduledFutureTask重写了run()方法:
public void run() {
// 判断是否重复执行
boolean periodic = isPeriodic();
// 线程池状态判断
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 如果不是周期性执行任务,则直接调用父类(FutureTask)的run()方法
else if (!periodic)
ScheduledFutureTask.super.run();
// 如果是周期性执行任务,先调用父类(FutureTask)的runAndReset()方法
else if (ScheduledFutureTask.super.runAndReset()) {
// 设置下次执行的时间
setNextRunTime();
// 重复执行
reExecutePeriodic(outerTask);
}
}
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
// 线程池状态检查
if (canRunInCurrentRunState(true)) {
// 再次把任务加入任务队列
super.getQueue().add(task);
// 再次检查线程池状态
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
// 保证工作线程足够
ensurePrestart();
}
}
由此可见,ScheduledFutureTask最主要的功能是根据当前任务是否具有周期性,对异步任务进行进一步封装。如果不是周期性任务则直接通过run()方法执行;若是周期性任务,则需要在每一次执行完后,重设下一次执行的时间,然后将下一次任务继续放入到任务队列中。
ⅱ、scheduleAtFixedRate()
同理,可以看到scheduleAtFixedRate()方法原理与schedule()方法类似。
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
// 参数判断
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
// 将传入的Runnable类装饰成ScheduledFutureTask类
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
// 延时执行
delayedExecute(t);
return t;
}
4、DelayedWorkQueue
DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面。
DelayedWorkQueue相关参数如下:
private static final int INITIAL_CAPACITY = 16;
// 数组元素为实现RunnableScheduleFuture接口的类(ScheduledFutureTask)
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
private int size = 0;
阻塞队列相关内容和思路与之前一致:阻塞队列
5、其他提交任务的方法
public void execute(Runnable command) {
schedule(command, 0, NANOSECONDS);
}
public Future<?> submit(Runnable task) {
return schedule(task, 0, NANOSECONDS);
}
可以看到,使用execute()方法和submit()方法也可以提交任务,但是会被当成0延时的任务来执行。
6、总结
实现定时任务有两个问题要解决,分别是指定未来某个时刻执行任务、重复执行:
●指定未来某个时刻执行任务,是通过延时队列解决的。
●重复执行,是通过在任务执行后再次把任务加入任务队列来解决的。
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,因此,整体上功能一致:线程池主要负责创建线程(Worker类),线程从阻塞队列中不断获取新的异步任务,直到阻塞队列中已经没有了异步任务为止。但是相比ThreadPoolExecutor,ScheduledThreadPoolExecutor具有延时执行任务和可周期性执行任务的特性,ScheduledThreadPoolExecutor重新设计了任务类ScheduleFutureTask,ScheduleFutureTask重写了run方法使其具有可延时执行和可周期性执行任务的特性。另外,阻塞队列DelayedWorkQueue是可根据优先级排序的队列,采用了堆的底层数据结构,使得与当前时间相比,待执行时间越靠近的任务放置队头,以便线程能够获取到任务进行执行。
⑨ForkJoinPool
前面简单提到ForkJoinPool是JDK7中新增的线程池类。它通常是以递归的方式运行,采用分治思想将大任务分割成几个小任务,小任务继续分割成更小的任务,直至任务不可分割,然后运行这些任务。因此,ForkJoinPool的适用范围不大,仅限于某个任务能被分解成多个子任务,且这些子任务运行的结果可以合并成最终结果。
前面也提到ForkJoinPool中实现了一种工作窃取算法,所谓工作窃取指的是闲置线程的任务队列空了,就从其他忙碌线程中的任务队列中处理任务。
ForkJoinPool不是为了替代 ExecutorService而生,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。ForkJoinPool最适合处理计算密集型的任务。
1、分治法
ⅰ、基本思想
把一个大规模的问题划分为多个较小规模的子问题,然后分而治之,最后合并子问题的解得到原问题的解。
ⅱ、基本步骤
●分割原问题。
●求解子问题。
●合并子问题的解为原问题的解。
在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题。
ⅲ、典型应用场景
●二分搜索
●大整数乘法
●Strassen矩阵乘法
●棋盘覆盖
●归并排序
●快速排序
●线性时间选择
●汉诺塔
2、内部结构
ⅰ、两个主要方法
fork() 方法类似于线程的 Thread.start() 方法,但是它不是真的启动一个线程,而是将任务放入到工作队列中。
public final ForkJoinTask<V> fork() {
Thread t;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
ForkJoinPool.common.externalPush(this);
return this;
}
join() 方法类似于线程的 Thread.join() 方法,但是它不是简单地阻塞线程,而是利用工作线程运行其它任务。当一个工作线程中调用了 join() 方法,它将处理其它任务,直到注意到目标子任务已经完成了。
public final V join() {
int s;
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);
return getRawResult();
}
ⅱ、三个子类
●RecursiveAction:无返回值任务。
●RecursiveTask:有返回值任务。
●CountedCompleter:无返回值任务,完成任务后可以触发回调。
3、poll()方法
“窃取”操作的实现就是 poll() 方法。
final ForkJoinTask<?> poll() {
ForkJoinTask<?>[] a; int b; ForkJoinTask<?> t;
// array就是双端队列,用数组实现
// base是将要偷的任务下标,base是用volatile修饰的,保证可见性
// top是将要push进去的任务下标
while ((b = base) - top < 0 && (a = array) != null) {
// 经过while条件初步判断任务队列不为空
// 获取base处的任务在任务队列中的偏移量
int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
// 用volatile load语义取出base处的任务t(可以简单理解为一定是最新修改版本的任务)
t = (ForkJoinTask<?>)U.getObjectVolatile(a, j);
// 再次读取base(判断此时t是否被别的线程偷走)
if (base == b) {
if (t != null) {
// 如果多次读判断都没问题,CAS修改base处的任务t为null
if (U.compareAndSwapObject(a, j, t, null)) {
// 如果判断中的修改成功,表示这个任务被该线程偷到了。此时就将base指针向前移一位(注意这一步是原子操作,base++就不是了)
base = b + 1;
return t;
}
}
// 如果t==null && b + 1 == top,说明此时任务队列为空
else if (b + 1 == top) // now empty
break;
}
}
return null;
}
因 poll() 方法是多个线程的同步操作,需要保证:偷到 Base 处的任务和 Base++ 的原子性,同时 Base 的值一旦改变,其他线程应该能够马上可见。
简单来说,有任务可偷时,通过 CAS 偷任务保证只有一个线程能偷成功,偷成功的这个线程接着修改 volatile base 指针,使得马上对其他线程可见。同时通过前面的多次读判断减少后期 CAS 并发的冲突概率。没任务可偷时,通过 CAS 偷任务失败可以判断出来。
4、push()方法
push()方法是任务队列所属的线程才能操作,天生线程安全,因此,不需要通过 CAS 或锁来保证同步,只需要原子的修改 top 处任务和 top 向前移一位就可以了。
final void push(ForkJoinTask<?> task) {
ForkJoinTask<?>[] a; ForkJoinPool p;
int b = base, s = top, n;
if ((a = array) != null) { // ignore if queue removed
int m = a.length - 1; // fenced write for task visibility
// 更新双端队列array的top处任务为task,直接原子更新,非CAS操作(因为这个方法只会被array所属的线程调用,所以这里是线程安全的)
U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
// top指针向前移一位
U.putOrderedInt(this, QTOP, s + 1);
if ((n = s - b) <= 1) {
// 说明未push前队列中最多有一个任务
if ((p = pool) != null)
// 此时唤醒其他等待的线程,表示整体pool中有事情可以做了
p.signalWork(p.workQueues, this);
}
else if (n >= m)
// 队列扩容
growArray();
}
}
此处的 Base 和 Top 指针会存在任务冲突吗?不会,因为两个指针都在向前移位,Base 永远追赶不上 Top。这个方法额外需要做的事情是唤醒空闲线程,表示有任务进来了, 判断队列是否需要扩容就好。
5、pop()方法
pop()方法虽然是任务队列所属的线程才能操作,但是当任务队列只有一个任务时,存在 poll 和 pop 的任务竞争。
final ForkJoinTask<?> pop() {
ForkJoinTask<?>[] a; ForkJoinTask<?> t; int m;
if ((a = array) != null && (m = a.length - 1) >= 0) {
for (int s; (s = top - 1) - base >= 0;) {
long j = ((m & s) << ASHIFT) + ABASE;
if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
break;
if (U.compareAndSwapObject(a, j, t, null)) {
U.putOrderedInt(this, QTOP, s);
return t;
}
}
}
return null;
}
原理和 poll 一致,当 CAS 修改 top - 1 处任务为空成功时,再更新 top 值为 top - 1。
注意 pop 并没有 poll 那么多次预读避免并发竞争,这是因为 pop 只有在任务队列中只有一个任务时,才会存在和 poll 的竞争问题。而 poll 操作随时可能存在多个其他线程的竞争问题。
6、基本原理
内部使用的是“工作窃取”算法实现。
●每个工作线程都有自己的工作队列 WorkQueue,这是一个双端队列(每个线程拥有自己的任务队列可以提高获取队列的并行度)。
●ForkJoinTask中fork的子任务,将放入运行该任务的 WorkQueue 的队头,工作线程将以 LIFO 的顺序来处理 WorkQueue 中的任务。
●为了最大化地利用CPU,空闲的线程将从其它线程的队列中“窃取”任务来执行(从工作队列的尾部窃取任务,以减少竞争)。此时为 FIFO 的顺序。
●双端队列的操作方式:push()/pop()仅在其所有者工作线程中调用,poll()是由其它线程窃取任务时调用的。
●当只剩下最后一个任务时,还是会存在竞争,是通过 CAS 来实现的。
7、使用示例
假设有一个求和的需求。
首先,自定义一个类继承 RecursiveTask 类,重写其compute()方法。
public class SumTask extends RecursiveTask<Long> {
private long[] numbers;
private int from;
private int to;
public SumTask(long[] numbers, int from, int to) {
this.numbers = numbers;
this.from = from;
this.to = to;
}
@Override
protected Long compute() {
// 拆分子任务的规则:
// 优化:当需要计算的数字小于6时,直接计算结果
if (to - from < 6) {
long total = 0;
for (int i = from; i <= to; i++) {
total += numbers[i];
}
return total;
// 否则,把任务一分为二,递归计算
} else {
int middle = (from + to) / 2;
// 构造子任务
SumTask taskLeft = new SumTask(numbers, from, middle);
SumTask taskRight = new SumTask(numbers, middle + 1, to);
// 将子任务添加到任务队列
taskLeft.fork();
taskRight.fork();
// 合并所有子任务的规则:所有子任务的结果相加
return taskLeft.join() + taskRight.join();
}
}
}
然后,构造一个 ForkJoinPool,把上面的求和 SumTask 放进去。
ForkJoinPool pool = new ForkJoinPool();
SumTask sumTask = new SumTask(numbers, 0, numbers.length-1)
long result = pool.invoke(sumTask);
System.err.println(result);
可以看到,把任务放进线程池是调用的 invoke() 方法。
8、归并任务的流转
ⅰ、将任务提交到任务队列
根据上面的例子进一步追溯 invoke() 方法,可以找到 ForkJoinPool 的 externalPush() 方法。其实所有任务提交方法最终都会调用该方法。
此时,任务被提交到哪个队列呢?如果提交到 ForkJoinWorkerThread 自己的双端任务队列中:不管提交到头还是尾,都会和上面的三个操作(poll、push、pop)发生任务冲突,而且如何选择负载最小的线程来提交也会增加问题复杂性。
在 ForkJoinPool 中双端任务队列是用数组(volatile WorkQueue[] workQueues)实现的,其中奇数下标存放的是可激活的任务队列,偶数下标存放的是不可激活的任务队列。(激活指的是这个队列是否是某个 ForkJoin 线程的任务队列)
ForkJoinPool.externalPush 只能将任务提交到不可激活任务队列,该方法的主要逻辑为:
●当提交的任务是 pool 的第一个任务时,会初始化 workQueues、ForkJoinWorkerThread 等资源,通过 hash 算法选择一个偶数下标的 workQueue,在 top 处放入任务。同时唤醒 ForkJoinWorkerThread 开始拉取任务工作。
●当提交的任务不是第一个任务,此时 workQueues 等资源已初始化好。同样需要选择一个偶数下标的 workQueue 存放任务,如果选中的 workQueue 只有这一个任务,说明之前线程资源大概率是闲置的状态,会尝试唤醒(signalWork() 方法) 一个空闲的 ForkJoinWorkerThread 开始拉取任务工作。
ⅱ、ForkJoinWorkerThread的运行
先看一下任务正常的生产和消费:
可激活的 workQueue 自己所属 ForkJoinWorkerThread 的任务模式是 LIFO,不可激活的 workQueue 的任务模式是 FIFO。
ForkJoinWorkerThread 刚开始运行时会调用 scan() 方法随机选取一个队列从 base 处捞取任务,捞取到任务会调用 WorkQueue.runTask 方法执行任务,最终对于 RecursiveTask 任务执行的是 RecursiveTask.exec 方法:
protected final boolean exec() {
// compute()方法是抽象的
result = compute();
return true;
}
fork 所做的事情就是将切分的子任务添加到当前 ForkJoinWorkerThread 自己的 workQueue 中:
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
join 所做的事情就是等待子任务的返回结果:
public final V join() {
int s;
// 获取doJoin的执行结果
if ((s = doJoin() & DONE_MASK) != NORMAL)
// 如果结果有异常,抛出异常信息
reportException(s);
// 如果结果无异常,返回正常结果
return getRawResult();
}
当获取 doJoin 的执行结果时,ForkJoinWorkerThread 会根据当前任务的情况,执行额外的优化操作,而不是单纯的阻塞等待结果。
ⅲ、join 时执行任务的判断
情况一:任务未被偷
假设任务被 Thread 执行,fork 出两个子任务:A 和 B,只要 Thread 能判断出要 join 的任务在自己的任务队列中,那当前 join 哪个子任务就把它取出来执行就可以了。
情况二:任务被“偷”,且自己的任务队列为空
假设任务被 Thread1 执行,fork 出两个子任务:A 和 B。B 已成功执行完成,join 返回了结果。但此时发现 A 被 Thread2 偷走了,自己的任务队列中已经没有任务可以执行了。此时 Thread1 可以找到小偷 Thread2,并偷取 Thread2 的 C 任务来执行。
情况三:任务被偷,且自己的任务队列不为空
假设任务被 Thread1 执行,fork 出两个子任务:A 和 B,要 join A 时发现已经被 Thread2 偷走了,而自己队列中还有 B 等待 join 执行。此时就帮不了小偷了。
此时尝试挂起自己,等待 A 的执行结果通知,并尝试唤醒空闲线程或者创建新的线程替代自己执行任务队列中的 B 任务。
总结整体思路是:
当任务还在自己的队列时:
●自己执行,获取结果。
当任务被别人偷走了:
●自己如果没任务执行,就帮助小偷执行任务。
●自己有任务要执行,就尝试挂起自己,等待小偷的反馈结果,同时找队友帮助自己执行。
9、总结
●ForkJoinPool特别适合于分治算法的实现。
●ForkJoinPool和ThreadPoolExecutor是互补的,不存在替代关系,二者适用的场景不同。
●ForkJoinTask有两个核心方法fork()和join(),有三个重要子类RecursiveAction、RecursiveTask和CountedCompleter。
●ForkjoinPool内部基于“工作窃取”算法实现。
●每个线程有自己的工作队列,它是一个双端队列,自己从队列头存取任务,其它线程从尾部窃取任务。
●ForkJoinPool最适合于计算密集型任务,如果需要阻塞可以使用ManagedBlocker。
●RecursiveTask内部可以少调用一次fork()方法,利用当前线程处理。
●scan 操作是从 base 处获取任务,那么更容易获取到大的任务执行,从而使得整体线程的资源分配更加均衡。
●任务队列所属的线程是 LIFO 的任务生产消费模式,刚好符合递归任务的执行顺序。
最佳实践
●最适合的是计算密集型任务。
●在需要阻塞工作线程时,可以使用ManagedBlocker(ForkJoinPool的内部接口)。
●不应该在RecursiveTask的内部使用ForkJoinPool.invoke()/invokeAll()。
PS:注意:ForkJoinPool在执行过程中,会创建大量的子任务,导致GC进行垃圾回收。
三、线程池的扩展:
①线程池功能扩展
在使用线程池的时候,能扩展线程池的功能吗?比如记录线程任务的执行时间等。实际上,JDK 的线程池已经预留了接口,在线程池核心方法中,有2个方法是空的,还有一个线程池退出时会调用的方法。
要扩展线程池的功能,可以重写 beforeExecute()、afterExecute()、terminated() 方法,这三个方法默认是空的。在 Worker 的 runWork() 方法中,会调用这些方法。
static class MyTask implements Runnable {
String name;
public MyTask (String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("正在执行:Thread ID:" + Thread.currentThread().getId() + ", Task Name = " + name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("准备执行:" + ((MyTask) r).name);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("执行完成:" + ((MyTask) r).name);
}
@Override
protected void terminated() {
System.out.println("线程池退出");
}
};
for (int i = 0; i < 5; i++) {
MyTask myTask = new MyTask("TASK-" + i);
es.execute(myTask);
Thread.sleep(10);
}
es.shutdown();
}
beforeExecute() 是执行任务之前会被调用,而 afterExecute() 则是在任务执行完毕后会被调用。还有一个 terminated(),在线程池退出时会被调用。
②优化线程池的异常信息
先来看一个bug场景:
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
for (int i = 0; i < 5; i++) {
executor.submit(new DivTask(100, i));
}
}
static class DivTask implements Runnable {
int a, b;
public DivTask(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
double re = a / b;
System.out.println(re);
}
}
运行后可以看到,只有4个结果,其中一个结果被吞没了,并且没有任何信息。仔细看代码,会发现,在进行 100 / 0 的时候肯定会报错的,但却没有报错信息,为什么呢?实际上,如果使用 execute() 则会打印错误信息,但使用 submit() 却没有调用它的 get(),异常将会被吞没,因为,如果发生了异常,异常是作为返回值返回的。
要解决这个问题,当然可以使用 execute(),但也有另一种方式:重写 submit()。比如:
static class TraceThreadPoolExecutor extends ThreadPoolExecutor {
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
// super.execute(command);
super.execute(wrap(command, clientTrace()));
}
@Override
public Future<?> submit(Runnable task) {
// return super.submit(task);
return super.submit(wrap(task, clientTrace()));
}
private Exception clientTrace() {
return new Exception("Client stack trace");
}
private Runnable wrap(final Runnable task, final Exception clientStack) {
return () -> {
try {
task.run();
} catch (Exception e) {
e.printStackTrace();
clientStack.printStackTrace();
throw e;
}
};
}
}
重写了 submit(),封装了异常信息,如果发生了异常,将会打印堆栈信息。使用时便可以看到错误信息,方便排错。
系列文章传送门:
JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal