java并发笔记

36 阅读23分钟

并发编程

进程和线程

进程是程序的一次执行过程,是系统运行程序的基本单位

线程是比进程更小的执行单位。一个进程执行过程中可以产生多个线程。多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈

  • 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
  • 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。

用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。

现在的 Java 线程的本质其实就是操作系统的线程

  • 程序计数器:线程切换后能恢复到正确的执行位置:
  • 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

堆和方法区是所有线程共享的资源

  1. 堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),
  2. 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

线程创建方式

  1. 继承 Thread 类,重写run方法
  2. 实现 Runnable 接口
  3. 实现 Callable 接口。
  • 继承 Thread 类,重写 run()方法,调用 start()方法启动线程,没有返回值
public class ThreadTest {
​
    /**
     * 继承Thread类
     */
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("This is child thread");
        }
    }
​
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
  • 实现 Runnable 接口,重写 run()方法,没有返回值
public class RunnableTask implements Runnable {
    public void run() {
        System.out.println("Runnable!");
    }
​
    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        new Thread(task).start();
    }
}
  • 实现 Callable 接口,重写 call()方法,可以通过 FutureTask 获取任务执行的返回值
public class CallerTask implements Callable<String> {
    public String call() throws Exception {
        return "Hello,i am running!";
    }
​
    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> task=new FutureTask<String>(new CallerTask());
        //启动线程
        new Thread(task).start();
        try {
            //等待执行完成,并获取返回结果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Start(), run()

JVM 执行 start 方法,会先创建一条线程,由创建出来的新线程去执行 thread 的 run 方法,这才起到多线程的效果。

直接调用 Thread 的 run()方法, run 方法还是运行在主线程中,相当于顺序执行,没有多线程效果。

守护线程

Java线程分为两类

  1. daemon 线程(守护线程)
  2. user 线程(用户线程)。

在 JVM 启动时会调用 main 方法,main 方法所在的线程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程, 比如垃圾回收线程。

当最后一个非守护线程束时, JVM 会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM 退出。只要有一个用户线程还没结束,正常情况下 JVM 就不会退出。

线程间通信方式

ThreadLocal

ThreadLocal,是线程本地变量。创建一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有变量的一个本地拷贝,多个线程操作这个变量实际是操作自己本地内存里的变量,从而起到线程隔离的作用,避免了线程安全问题。

ThreadLocal 的 set(T)方法中,先获取到当前线程,再获取ThreadLocalMap,然后把元素存到这个 map 中。Thread 类中定义了一个类型为ThreadLocal.ThreadLocalMap的成员变量threadLocals

  • Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,每个线程都有一个自己的 ThreadLocalMap。
  • ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 的弱引用,value 是 ThreadLocal 的泛型值。
  • 每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。
  • ThreadLocal 本身不存储值,它只是作为一个 key 来让线程往 ThreadLocalMap 里存取值。

内存泄漏

在 JVM 中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。栈中存储了 ThreadLocal、Thread 的引用,堆中存储了它们的具体实例。

ThreadLocal内存分配

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用。

“弱引用:只要垃圾回收机制一运行,不管 JVM 的内存空间是否充足,都会回收该对象占用的内存。”

弱引用很容易被回收,如果 ThreadLocal(ThreadLocalMap 的 Key)被垃圾回收器回收了,但是 ThreadLocalMap 生命周期和 Thread 是一样的,它这时候如果不被回收,就会出现:ThreadLocalMap 的 key 没了,value 还在,这就会造成了内存泄漏问题

那怎么解决内存泄漏问题呢?

使用完 ThreadLocal 后,及时调用 remove()方法释放内存空间。

那为什么 key 还要设计成弱引用?

key 设计成弱引用同样是为了防止内存泄漏。

假如 key 被设计成强引用,如果 ThreadLocal Reference 被销毁,此时它指向 ThreadLocal 的强引用就没有了,但是此时 key 还强引用指向 ThreadLocal,就会导致 ThreadLocal 不能被回收,也会发生内存泄漏

扩容机制

在 ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:

rehash()具体实现:会先去清理过期的 Entry,然后还要根据条件判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4来决定是否需要扩容。

具体的resize()方法,扩容后的newTab的大小为老数组的两倍,然后遍历老的 table 数组,散列方法重新计算位置,开放地址解决冲突,然后放到新的newTab,遍历完成后,oldTab中所有的entry数据都已经放入到newTab中了,然后 table 引用指向newTab

引用方式

  1. 强引用:new出来的对象,强引用存在,垃圾回收器永远不会回收被引用的对象,哪怕内存不足
  2. 软引用:SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出时被回收
  3. 弱引用:WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若对象只被弱引用指向,就会被回收
  4. 虚引用:最弱的引用,在 Java 中使用 PhantomReference 进行定义。唯一的作用就是用队列接收对象即将死亡的通知

父子线程共享数据

JAVA内存模型JMM

Java Memory Model,是一种抽象的模型,被定义出来屏蔽各种硬件和操作系统的内存访问差异。

JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

本地内存是 JMM 的抽象概念,实际不存在。涵盖缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

原子性、可见性、有序性

  • 原子性:原子性指的是一个操作不可分割、不可中断,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。
  • 可见性:一个线程修改了某一共享变量的值后,其它线程能立即知道这个修改。
  • 有序性:对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,并发时有可能会指令重排。

原子性、可见性、有序性都应该怎么保证呢?

  • 原子性:JMM 只能保证基本的原子性,保证代码块的原子性需要使用synchronized
  • 可见性:Java 是利用volatile关键字来保证可见性的,finalsynchronized也能保证可见性。
  • 有序性:synchronized或者volatile都可以保证多线程之间操作的有序性。

指令重排

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分 3 种类型。

  1. 编译器优化。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行。现代处理器采用了指令级并行技术将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

双重校验单例模式就是经典的指令重排的例子,Singleton instance=new Singleton();对应的 JVM 指令分为三步:分配内存空间-->初始化对象--->对象指向分配的内存空间,但是经过编译器指令重排序,第二步和第三步就可能会重排序。

Happens-before

指令重排也有一些限制,有两个规则happens-beforeas-if-serial约束。

happens-before 的定义:

  • 如果操作A happens-before操作B,那么操作A的执行结果将对操作B可见,且操作A的执行顺序排在操作B之前。
  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,重排序并不非法
  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
  • start()规则:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
  • join()规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。

as-if-serial

as-if-serial 语义是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

volatile

作用:保证可见性和有序性

可见性原理:volatile 可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。

有序性原理:为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  1. 在每个 volatile 写操作的前面插入一个StoreStore屏障
  2. 在每个 volatile 写操作的后面插入一个StoreLoad屏障
  3. 在每个 volatile 读操作的后面插入一个LoadLoad屏障
  4. 在每个 volatile 读操作的后面插入一个LoadStore屏障

Load:从主内存中加载

Store:写入主内存

synchronized

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
  • 修饰静态方法:也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,只有⼀份)。
  • 修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized(类.class) 表示进⼊同步代码前要获得 当前 class 的锁

原理

  • 修改代码块:JVM 采用monitorentermonitorexit两个指令来实现同步,monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。
  • 修饰同步方法时,JVM 采用ACC_SYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。

锁升级

锁升级简略过程

synchronized,ReentrantLock

  • 锁的实现: synchronized 是 Java 语言的关键字,基于 JVM 实现。而 ReentrantLock 是基于 JDK 的 API 层面实现的(一般是 lock()和 unlock()方法配合 try/finally 语句块来完成。)

  • 性能: 在 JDK1.6 锁优化以前,synchronized 的性能比 ReenTrantLock 差很多。 JDK6 后增加了适应性自旋、锁消除等,两者性能差不多。

  • 功能特点:ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。

    • ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()实现
    • ReentrantLock 可以公平锁、非公平锁。synchronized 只能非公平锁。公平锁就是先等待的线程先获得锁。
    • synchronized 与 wait()和 notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock 类借助 Condition 接口与 newCondition()方法实现。
    • ReentrantLock 需要手工声明来加锁和释放锁,一般跟 finally 配合释放锁。而 synchronized 不用手动释放锁。

synchronized和ReentrantLock的区别

优化过程:

无锁

偏向锁

偏向锁主要用来优化同一线程多次申请同一个锁的竞争。在某些情况下,大部分时间是同一个线程竞争锁资源,例如,在创建一个线程并在线程中执行循环监听的场景下,或单线程操作一个线程安全集合时,同一线程每次都需要获取和释放锁,每次操作都会发生用户态与内核态的切换。

偏向锁的作用就是,当一个线程再次访问这个同步代码或方法时,该线程只需去对象头的 Mark Word 中去判断一下是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标志位设置为 1,并且记录抢到锁的线程 ID,表示进入偏向锁状态。

一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占。

轻量级锁

当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。

轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。

重量级锁

轻量级锁 CAS 抢锁失败,线程将会被挂起进入阻塞状态。如果正在持有锁的线程在很短的时间内释放资源,那么进入阻塞状态的线程无疑又要申请锁资源。

JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。

从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定,不建议设置的重试次数过多,因为 CAS 重试操作意味着长时间地占用 CPU。

自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。

AQS

AbstractQueuedSynchronizer抽象同步队列简称 AQS ,是 Java 并发包的根基,并发包中的锁基于 AQS 实现

  • AQS 是基于一个 FIFO 的双向队列,内部定义了一个节点类 Node,Node 节点内部的 SHARED 用来标记该线程是获取共享资源时被阻挂起后放入 AQS 队列的, EXCLUSIVE 用来标记线程是 取独占资源时被挂起后放入 AQS 队列
  • AQS 使用一个 volatile 修饰的 int 类型的成员变量 state 来表示同步状态,修改同步状态成功即为获得锁,volatile 保证了变量在多线程之间的可见性,修改 State 值时通过 CAS 机制来保证修改的原子性
  • 获取 state 的方式分为两种,独占方式和共享方式,一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过 CAS 的方式进行获取。
  • 如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到一个变体的 CLH 队列中。

ReentrantLock实现原理

ReentrantLock 是可重入的独占锁,只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。

默认创建的对象 lock()的时候:

  • 如果锁当前没有被其它线程占用,并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1 ,然后直接返回。如果当前线程之前己经获取过该锁,则这次只是简单地把 AQS 的状态值加 1 后返回。
  • 如果该锁己经被其他线程持有,非公平锁会尝试去获取锁,获取失败的话,则调用该方法线程会被放入 AQS 队列阻塞挂起。

ReentrantLock 非公平锁加锁流程简图

非公平锁和公平锁的两处不同:

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

保证原子性的方法

多线程下i++结果正确

  • 使用循环原子类,例如 AtomicInteger,实现 i++原子操作
  • 使用 juc 包下的锁,如 ReentrantLock ,对 i++操作加锁 lock.lock()来实现原子性
  • 使用 synchronized,对 i++操作加锁

死锁排查

可以使用 jdk 自带的命令行工具排查:

  1. 使用 jps 查找运行的 Java 进程:jps -l
  2. 使用 jstack 查看线程堆栈信息:jstack -l 进程 id

CountDownLatch

  1. 场景 1:协调子线程结束动作:等待所有子线程运行结束
  2. 场景 2. 协调子线程开始动作:统一各线程动作开始的时机

CyclicBarrier同步屏障

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CountDownLatch 类似,都可以协调多线程的结束动作,在它们结束后都可以执行特定动作

CyclicBarrier 和 CountDownLatch

两者最核心的区别[18]:

  • CountDownLatch 是一次性的,而 CyclicBarrier 则可以多次设置屏障,实现重复利用;
  • CountDownLatch 中的各个子线程不可以等待其他线程,只能完成自己的任务;而 CyclicBarrier 中的各个线程可以等待其他线程
CyclicBarrierCountDownLatch
CyclicBarrier 是可重用的,其中的线程会等待所有的线程完成任务。届时,屏障将被拆除,并可以选择性地做一些特定的动作。CountDownLatch 是一次性的,不同的线程在同一个计数器上工作,直到计数器为 0.
CyclicBarrier 面向的是线程数CountDownLatch 面向的是任务数
在使用 CyclicBarrier 时,你必须在构造中指定参与协作的线程数,这些线程必须调用 await()方法使用 CountDownLatch 时,则必须要指定任务数,至于这些任务由哪些线程完成无关紧要
CyclicBarrier 可以在所有的线程释放后重新使用CountDownLatch 在计数器为 0 时不能再使用
在 CyclicBarrier 中,如果某个线程遇到了中断、超时等问题时,则处于 await 的线程都会出现问题在 CountDownLatch 中,如果某个线程出现问题,其他线程不受影响

Semaphore信号量

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger 用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。

这两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange()方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

线程池

工作流程

线程池执行流程

核心参数

线程池参数

常见线程池

线程池名称线程数量队列数量说明
fixedThreadPool SingleThreadExecutor固定没有上限
cachedThreadPool没有上限没有数据缓冲的阻塞队列,生产后必须消费
scheduledThreadPool没有上限的延迟阻塞队列

阻塞队列

线程池常用阻塞队列

  • ArrayBlockingQueue:有界队列,是一个用数组实现的有界阻塞队列,按 FIFO 排序量。
  • LinkedBlockingQueue:可设置容量队列,是基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择设置,默认是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE,吞吐量通常要高于 ArrayBlockingQuene;newFixedThreadPool 线程池使用了这个队列
  • DelayQueue:延迟队列,是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool 线程池使用了这个队列。
  • PriorityBlockingQueue:优先级队列,是具有优先级的无界阻塞队列
  • SynchronousQueue:同步队列,一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQuene,newCachedThreadPool 线程池使用了这个队列。

execute和submit

execute 用于提交不需要返回值的任务

threadsPool.execute(new Runnable() {
    @Override public void run() {
        // TODO Auto-generated method stub }
    });

submit()方法用于提交需要返回值的任务。线程池会返回一个 future 类型对象,通过 future 对象可以判断任务是否执行成功,可以通过 future.get()方法获取返回值

Future<Object> future = executor.submit(harReturnValuetask);
try { Object s = future.get(); } catch (InterruptedException e) {
    // 处理中断异常
} catch (ExecutionException e) {
    // 处理无法执行任务异常
} finally {
    // 关闭线程池 executor.shutdown();
}

关闭方式

可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。

shutdown() 将线程池状态置为 shutdown,并不会立即停止

  1. 停止接收外部 submit 的任务
  2. 内部正在跑的任务和队列里等待的任务,会执行完
  3. 等到第二步完成后,才真正停止

shutdownNow() 将线程池状态置为 stop。一般会立即停止,事实上不一定

  1. 和 shutdown()一样,先停止接收外部提交的任务
  2. 忽略队列里等待的任务
  3. 尝试将正在跑的任务 interrupt 中断
  4. 返回未执行的任务列表

区别:

  • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。立即生效,风险比较大。
  • shutdown()只是关闭了提交通道,submit()是无效的;而内部的任务正常进行,跑完再彻底停止线程池。

拒绝策略

  • AbortPolicy :直接抛出异常,默认使用此策略
  • CallerRunsPolicy:用调用者所在的线程来执行任务
  • DiscardOldestPolicy:丢弃阻塞队列里最老的任务,也就是队列里靠前的任务
  • DiscardPolicy :当前任务直接丢弃

设计实现一个线程池

Fork/Join