Java 多线程与并发答案 - 图1

Java 并发 - 理论基础

为什么需要多线程④

  1. CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能
  2. CPU 增加了缓存,导致 可见性问题
  3. 操作系统增加了进程、线程,以分时复用 CPU, 导致原子性问题
  4. 编译程序优化指令执行次序,导致 有序性问题

并发出现问题的根源

  1. 并发三要素
  2. 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
  3. 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  4. 有序性:即程序执行的顺序按照代码的先后顺序执行
  5. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新 安排语句的执行顺序。
  6. 指令级并行的重排序
  7. 内存系统的重排序

JAVA是怎么解决并发问题

  • 可以通过什么保证原子性,可见性,有序性
  • JMM是通过什么来保证有序性的
  1. Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法
  2. 方法包括:
  3. volatilesynchronized final 三个关键字
  4. 共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存
  5. 通过volatile关键字来保证一定的“有序性”
  6. Happens-Before 规则
  7. 可以通过synchronizedLock保证原子性,可见性,有序性
  8. JMM是通过Happens-Before 规则来保证有序性的

Happens-Before 规则

  • 单一线程原则
  1. 在一个线程内,在程序前面的操作先行发生于后面的操作。
  • 管程锁定规则
  1. 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 变量规则
  1. 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则
  1. Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
  • 线程加入规则
  1. Thread 对象的结束早于 join() 方法返回。
  2. A线程调用B线程.join,等B线程结束唤醒A
  • 对象终结规则
  1. 一个对象的初始化完成早于它 finalize() 方法的开始。
  • 线程中断规则
  1. 对线程 interrupt() 早于interrupted() 方法
  • 传递性
  1. A早于B,B早于C,A早于C

线程安全

  • 可以将共享数据按照安全程度的强弱顺序分成以下五类:
    1. 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
  • 不可变
  1. 多线程环境下,应当尽量使对象成为不可变,来满足线程安全
  1. 不可变的类型
  1. final修饰的基本数据类型
  2. String
  3. 枚举类型
  4. Number 部分子类(Long Double 等数值包装类型,BigInteger BigDecimal 等大数据类型)
  1. 对于集合类型,可以使用什么方法来获取一个不可变的集合
  1. Collections.unmodifiableXXX()
  2. 对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
  • 绝对线程安全
  1. 不管运行时环境如何,调用者都不需要任何额外的同步措施
  • 相对线程安全
  1. 保证对这个对象单独的操作是线程安全,调用不用做额外措施,但特殊连续调用需要同步手段
  2. VectorHashTableCollections synchronizedCollection() 方法包装的集合
  • 线程兼容
  1. 对象本身并不是线程安全,通过同步手段可以同步,例 ArrayList HashMap
  • 线程对立
  1. 怎么都不能同步

线程安全的实现方法

互斥同步

  • 最大问题
    1. 线程阻塞和唤醒所带来的性能问题
  • 属于什么的并发策略,哪些操作
    1. 加锁,用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒
  • synchronized 和 ReentrantLock

非阻塞同步

  • CAS

    • 属于什么类
      1. Unsafe
  • 是什么
  1. 操作和冲突检测原子操作
  • 检测到冲突

    1. 不断地重试,直到成功为止
  • 实现

    1. CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B
  • AtomicInteger

    • compareAndSet()
    • getAndIncrement()
  • ABA

    • 变量初次读取是 A ,被改成 B,后来又改回 A,那 CAS 操作就认为它没有被改过

    • J.U.C 包提供了一个带有标记的类 什么来解决这个问题

      1. AtomicStampedReference
      2. 通过控制变量值的版本来保证 CAS 的正确性
  • 解决 ABA 问题,改用什么可能会比原子类更高效
    1. 传统的互斥同步

无同步方案

  • 栈封闭
  1. 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的
  • 线程本地存储
  1. 使用 java.lang.ThreadLocal 类来实现线程本地存储功能
  2. 每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员
  • 可重入代码
  1. 可在任意时刻中断去执行其他,再接着执行不会出错
  2. 不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法

Java 并发 - 线程基础

Java 多线程与并发答案 - 图2

线程状态转换

  • 新建(New)
    1. 新建未启动
  • 可运行(Runnable)② ``` 可能正在运行,也可能正在等待 CPU 时间片。

包含了操作系统线程状态中的 Running 和 Ready。

  1. -
  2. 阻塞(Blocking)
  3. -
  4. 无限期等待(Waiting)
  5. -
  6. 进入方法和退出方法

1.没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll() 2.没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕 3.LockSupport.park() 方法

  1. -
  2. 是什么
  3. -
  4. 限期等待(Timed Waiting)
  5. -
  6. 是什么
  7. -
  8. 进入方法和退出方法

1.Thread.sleep() 方法 时间结束 2.设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll() 3.设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕 4.LockSupport.parkNanos() 方法 5.LockSupport.parkUntil() 方法

  1. -
  2. 睡眠和挂起是用来描述,而阻塞和等待用来描述(主动和被动)。
  3. -
  4. 死亡(Terminated)
  5. - 二种情况
  6. <a name="a17fcdd0"></a>
  7. ## 线程使用方式
  8. -
  9. 实现 Runnable 接口
  10. -
  11. 实现 Callable 接口
  12. - Runnable 相比,Callable 可以有什么,返回值通过。

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

  1. -
  2. 继承 Thread
  3. -
  4. 需要实现,因为

需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口

  1. -
  2. 当调用 start() 方法启动一个线程时

虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法

  1. -
  2. 实现接口会更好一些,因为

Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; 类可能只要求可执行就行,继承整个 Thread 类开销过大。

  1. <a name="5d01373d"></a>
  2. ## 基础线程机制
  3. -
  4. Executor
  5. -
  6. 是什么

Executor管理多个异步任务的执行,无需程序员显式地管理线程的生命周期,异步就是多个任务互不干扰,不用同步.

  1. -
  2. 三种Executor

CachedThreadPool: 一个任务创建一个线程 FixedThreadPool: 所有任务只能使用固定大小的线程 SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool

  1. -
  2. Daemon
  3. -
  4. 是什么

程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分

  1. -
  2. 当所有非守护线程结束时

程序也就终止,同时会杀死所有守护线程

  1. -
  2. main() 属于

非守护线程

  1. -
  2. 使用什么 方法将一个线程设置为守护线程

使用 setDaemon() 方法将一个线程设置为守护线程

  1. -
  2. sleep()
  3. -
  4. 使用方法,需要

Thread.sleep(millisec) 方法,sleep() 可能会抛出 InterruptedException,需要在本地进行处理

  1. -
  2. yield()

重要的已经执行完,可以切换到其他线程,只是对线程调度器的一个建议

  1. <a name="aada9e72"></a>
  2. ## 线程中断
  3. -
  4. 什么时候会中断

程序结束或异常时

  1. -
  2. InterruptedExcept

调用 interrupt() 中断线程 线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,提前结束该线程 不能中断 I/O 阻塞和 synchronized 锁阻塞

  1. -
  2. interrupted()

线程run()执行无限循环操作 没有执行 sleep() 等会抛出 InterruptedException 的操作 interrupt() 方法就无法使线程提前结束 但interrupt() 方法会设置线程的中断标记,调用 interrupted() 方法会返回 true

  1. -
  2. Executor 的中断操作
  3. -
  4. shutdown() 方法

等待线程都执行完毕之后再关闭

  1. -
  2. shutdownNow()

调用每个线程的 interrupt() 方法

  1. -
  2. 只想中断 Executor 中的一个线程

使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象 调用该对象的 cancel(true) 方法

  1. <a name="9d7ab9a6"></a>
  2. ## 线程互斥同步
  3. -
  4. 两种锁机制

控制多个线程对共享资源的互斥访问 JVM 实现的 synchronized JDK 实现的 ReentrantLock

  1. -
  2. synchronized

只作用于同一个对象

  1. -
  2. 同步一个代码块
  3. -
  4. 同步一个方法

同步一个类public synchronized void func () { // … }

  1. -
  2. 同步一个类

public void func() { synchronized (SynchronizedExample.class) { // … } } 两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步

  1. -
  2. 同步一个静态方法

public synchronized static void fun() { // … }

  1. -
  2. ReentrantLock
  3. - 是什么

是 java.util.concurrent(J.U.C)包中的锁

  1. -
  2. 比较
  3. -
  4. 锁的实现
  5. -
  6. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同

  1. -
  2. 等待可中断

ReentrantLock 可中断,而 synchronized 不行

  1. -
  2. 公平锁

多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁 synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的

  1. -
  2. 锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象

  1. -
  2. 使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized synchronized原生支持,ReentrantLock 不是所有的 JDK 版本都支持 使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放

  1. <a name="7c1ae05f"></a>
  2. ## 线程之间的协作
  3. -
  4. join()

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束

  1. -
  2. wait() notify() notifyAll()
  3. - 属于
  4. - 只能用在
  5. -
  6. wait() sleep() 的区别

wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; wait() 会释放锁,sleep() 不会。

  1. -
  2. await() signal() signalAll()
  3. -
  4. 在哪调用

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调 可以在 Condition 上调用 await() 方法使线程等 其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程 相比于 wait() 这种等待方式,await() 可以指定等待的条件

private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); condition.await(); condition.signalAll();

  1. <a name="b605494c"></a>
  2. # synchronized详解
  3. <a name="2f543f28"></a>
  4. ## Synchronized的使用
  5. -
  6. synchronized是通过实现的

软件(JVM)

  1. -
  2. 注意
  3. - 一把锁只能
  4. - 实例对象的锁对象
  5. - *.classsynchronized修饰的static方法锁对象
  6. - synchronized修饰的方法结束时②
  7. - 锁对象不能
  8. - 作用域
  9. - 同步时选择

1.一把锁只能同时被一个线程获取 2.实例对象都有自己的锁(this),锁对象是*.class或synchronized修饰的是static方法,所有对象公用一把锁 3.synchronized修饰的方法,正常结束或异常结束都会释放锁 4.锁对象不能为空,因为锁的信息都保存在对象头里 5.作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错 6.在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键字,因为代码量少,避免出错

  1. -
  2. 对象锁②

方法锁(默认锁对象为this) 同步代码块锁(自己指定锁对象)

  1. -
  2. 类锁

synchronize修饰静态的方法或指定锁对象为Class对象

  1. <a name="d10931c0"></a>
  2. ## Synchronized原理分析
  3. -
  4. 加锁和释放锁的原理

Monitorenter和Monitorexit指令会让对象在执行,使其锁计数器加1或者减1 每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得 一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一: monitor计数器为0, +1 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁, +1 这把锁已经被别的线程获取了,等待锁释放 monitorexit指令:monitor的计数器-1

  1. ![](https://pdai.tech/_images/thread/java-thread-x-key-schronized-2.png#alt=img)
  2. -
  3. 可重入原理

如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁, +1

  1. -
  2. 保证可见性的原理

在释放锁之前一定会将数据写回主内存 在获取锁之后一定从主内存中读取数据

  1. <a name="af719013"></a>
  2. ## JVM中锁的优化
  3. -
  4. JDK锁的优化

JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

  1. -
  2. JVMmonitorentermonitorexit字节码

JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现

  1. -
  2. 锁优化⑤
  3. -
  4. 锁的类型
  5. - 可以
  6. - 锁膨胀方向
  7. -
  8. 自旋锁与自适应自旋锁
  9. - 默认的自旋次数
  10. -
  11. 锁消除

虚拟机即时编译器对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除 例:Java API中有很多方法 例:String是一个不可变类,对字符串的连接操作总是通过生成的新的String对象来进行的, 因此Javac编译器会对String连接做自动优化。在JDK 1.5之前会使用StringBuffer对象的连续append()操作,在JDK 1.5及以后的版本中,会转化为StringBuidler对象的连续append()操作。

  1. -
  2. 锁粗化
  3. -
  4. 轻量级锁
  5. -
  6. 轻量级锁加锁

1.JVM在线程栈帧中创建锁记录Lock Record(锁记录用来存储锁对象目前的Mark Word拷贝,对象Mark Word标记字段为01表示未锁) 2.CAS操作把锁对象目前的Mark Word拷贝到线程栈桢锁记录 3.将Mark Word更新为指向锁记录的指针,把对象Mark Word标记字段更新为00

  1. ![](https://pdai.tech/_images/thread/java-thread-x-key-schronized-7.png#alt=img)
  2. -
  3. 偏向锁
  4. -
  5. 偏向锁的撤销
  6. <br />![](https://pdai.tech/_images/thread/java-thread-x-key-schronized-8.png#alt=img)
  7. ![](https://pdai.tech/_images/thread/java-thread-x-key-schronized-9.png#alt=img)
  8. - 锁的优缺点对比
  9. - 优点 缺点 使用场景
  10. <a name="00399da4"></a>
  11. ## Synchronized与Lock
  12. -
  13. synchronized的缺陷

1.效率低 2.不够灵活 3.无法知道是否成功获得锁

  1. -
  2. Lock解决相应问题
  3. -
  4. 4个方法
  5. -
  6. Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活解决办法
  7. -
  8. 多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断

ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待 有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。

  1. <a name="b44656a7"></a>
  2. # 关键字: volatile详解
  3. <a name="8c0f0a87"></a>
  4. ## volatile的作用详解
  5. -
  6. 防重排序

防止指令重排序

实例化一个对象其实可以分为三个步骤 分配内存空间。 初始化对象。 将内存空间的地址赋值给对应的引用

操作系统可以对指令进行重排序,可能变成 分配内存空间。 将内存空间的地址赋值给对应的引用。 初始化对象

  1. -
  2. 实现可见性
  3. -
  4. 保证原子性:单次读/写
  5. -
  6. 共享的longdouble变量的为什么要用volatile?

目前各种平台下的商用虚拟机都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不把long 和 double 变量专门声明为 volatile多数情况下也是不会错的。

  1. <a name="94e9f561"></a>
  2. ## volatile 的实现原理
  3. -
  4. volatile 可见性实现
  5. -
  6. volatile 变量的内存可见性是基于

volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现

内存屏障,又称内存栅栏,是一个 CPU 指令。 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止+ 特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序

  1. -
  2. volatile 修饰的共享变量进行写操作的时候

对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存 在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令 0x000000000295158c: lock cmpxchg %rdi,(%rdx)

  1. -
  2. lock 前缀的指令在多核处理器下会引发两件事情

将当前处理器缓存行的数据写回到系统内存。 写回内存的操作会使在其他 CPU 里缓存了该内存地址的额数据无效

  1. -
  2. lock 指令

lock 前缀会使处理器执行当前指令时产生一个 LOCK# 信号,会对总线进行锁定 后来由高速缓存锁代替总线锁来处理

  1. -
  2. volatile 有序性实现
  3. -
  4. volatile happens-before 关系

对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

  1. -
  2. volatile 禁止重排序
  3. <br />内存屏障指令

内存屏障指令 1.StoreStore 屏障 禁止上面的普通写和下面的 volatile 写重排序。 2.StoreLoad 屏障 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。 3.LoadLoad 屏障 禁止下面所有的普通读操作和上面的 volatile 读重排序。 4.LoadStore 屏障 禁止下面所有的普通写操作和上面的 volatile 读重排序。

  1. ![](https://pdai.tech/_images/thread/java-thread-x-key-volatile-3.png#alt=img)
  2. ![](https://pdai.tech/_images/thread/java-thread-x-key-volatile-4.png#alt=img)
  3. <a name="710e0c96"></a>
  4. ## volatile 的应用场景
  5. -
  6. 使用 volatile 必须具备的条件

对变量的写操作不依赖于当前值。 该变量没有包含在具有其他变量的不变式中。 只有在状态真正独立于程序内其他内容时才能使用 volatile。

  1. -
  2. 模式1:状态标志
  3. -
  4. 模式2:一次性安全发布
  5. -
  6. 模式3:独立观察
  7. -
  8. 模式4volatile bean 模式
  9. -
  10. 模式5:开销较低的读-写锁策略
  11. -
  12. 模式6:双重检查
  13. <a name="676075dd"></a>
  14. # 关键字: final详解
  15. <a name="588b95d1"></a>
  16. ## final基础使用
  17. -
  18. 修饰类
  19. -
  20. 继承
  21. -
  22. final类中的所有方法
  23. -
  24. final类型的类如何拓展

组合 在新类中创建旧类的实例对象调用实例方法.

  1. -
  2. 修饰方法
  3. - private 方法
  4. - final方法可以
  5. -
  6. 修饰参数
  7. - 无法在方法中
  8. - 主要用来
  9. -
  10. 修饰变量
  11. -
  12. 所有的final修饰的字段都是编译期常量吗

不是 Random r = new Random(); final int k = r.nextInt();

  1. -
  2. static final
  3. - 只占据
  4. - 赋值时机
  5. - 属于这个类
  6. -
  7. blank final
  8. - 赋值时机
  9. <a name="f754dbac"></a>
  10. ## final域重排序规则
  11. -
  12. final域为基本类型
  13. -
  14. final域重排序规则②

禁止对final域的写重排序到构造函数之外

  1. -
  2. final域重排序规则

在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用

  1. -
  2. final域为引用类型
  3. -
  4. final修饰的对象的成员域写操作

构造方法中引用类型赋值完后才能将构造出的对象赋值给应用

  1. -
  2. final修饰的对象的成员域读操作

只有构造方法中的写入先于其他的读操作

  1. <a name="43607421"></a>
  2. ## final再深入理解
  3. -
  4. final的实现原理②

写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。

  1. -
  2. 为什么final引用不能从构造函数中“溢出”

public FinalReferenceEscapeDemo() { a = 1; //1 referenceDemo = this; //2 }

本来在引用对象对所有线程可见时,其final域已经完全初始化成功。 但引用对象this溢出,读取对象时可能没初始化完

  1. -
  2. 使用 final 的限制条件和局限性
  3. -
  4. 当声明一个 final 成员时,必须

当声明一个 final 成员时,必须在构造函数退出前设置它的值

  1. -
  2. final指向引用对象
  3. <a name="ed6380ef"></a>
  4. # JUC - 类汇总和学习指南
  5. <a name="81012872"></a>
  6. ## JUC主要包含哪几部分
  7. <a name="f4298388"></a>
  8. ## Lock框架和Tools类
  9. -
  10. 接口: Condition

Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用

  1. -
  2. 接口: Lock

Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作接口:

  1. -
  2. 接口: ReadWriteLock

维护了一对相关的锁,一个用于只读操作,另一个用于写入操作 只要没有 writer,读取锁可以由多个 reader 线程同时保持 写入锁是独占的

  1. -
  2. 核心抽象类(int): AbstractQueuedSynchonizer

实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架

  1. -
  2. 锁常用类: LockSupport

用来创建锁和其他同步类的基本线程阻塞原语 功能和”Thread中的 Thread.suspend()和Thread.resume()有点类似” LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程 但是park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。

  1. -
  2. 锁常用类: ReentrantLock

可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大

  1. -
  2. 锁常用类: ReentrantReadWriteLock

ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁。

  1. -
  2. 锁常用类: StampedLock

java8在java.util.concurrent.locks新增的一个API StampedLock控制锁有三种模式(写,读,乐观读)

  1. -
  2. 工具常用类: CountDownLatch

同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待

  1. -
  2. 工具常用类: CyclicBarrier

同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点

  1. -
  2. 工具常用类: Phaser

JDK 7新增的一个同步辅助类,它可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。

  1. -
  2. 工具常用类: Semaphore
  3. -
  4. 工具常用类: Exchanger

线程协作的工具类, 主要用于两个线程之间的数据交换 它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据 这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法

  1. <a name="bcbd3f58"></a>
  2. ## Collections: 并发集合
  3. -
  4. Queue: ArrayBlockingQueue
  5. -
  6. Queue: LinkedBlockingQueue
  7. -
  8. Queue: LinkedBlockingDeque
  9. -
  10. Queue: ConcurrentLinkedQueue
  11. -
  12. Queue: ConcurrentLinkedDeque

是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式

  1. -
  2. Queue: DelayQueue

延时无界阻塞队列,使用Lock机制实现并发访问。队列里只允许放可以“延期”的元素,队列中的head是最先“到期”的元素。如果队里中没有元素到“到期”,那么就算队列中有元素也不能获取到。

  1. -
  2. Queue: PriorityBlockingQueue

无界优先级阻塞队列,使用Lock机制实现并发访问。priorityQueue的线程安全版,不允许存放null值,依赖于comparable的排序,不允许存放不可比较的对象类型。

  1. -
  2. Queue: SynchronousQueue

没有容量的同步队列,通过CAS实现并发访问,支持FIFO和FILO。

  1. -
  2. Queue: LinkedTransferQueue

JDK 7新增,单向链表实现的无界阻塞队列,通过CAS实现并发访问,队列元素使用 FIFO(先进先出)方式。LinkedTransferQueue可以说是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集, 它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。

  1. -
  2. List: CopyOnWriteArrayList
  3. -
  4. Set: CopyOnWriteArraySet

对其所有操作使用内部CopyOnWriteArrayList的Set。即将所有操作转发至CopyOnWriteArayList来进行操作,能够保证线程安全。在add时,会调用addIfAbsent,由于每次add时都要进行数组遍历,因此性能会略低于CopyOnWriteArrayList

  1. -
  2. Set: ConcurrentSkipListSet

一个基于ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现。set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator 进行排序,具体取决于使用的构造方法

  1. -
  2. Map: ConcurrentHashMap
  3. -
  4. Map: ConcurrentSkipListMap

线程安全的有序的哈希表(相当于线程安全的TreeMap);映射可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的 Comparator 进行排序,具体取决于使用的构造方法

  1. <a name="12cfc650"></a>
  2. ## Atomic: 原子类
  3. -
  4. 是什么

多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成 借助硬件的相关指令来实现的

  1. -
  2. 基础类型:AtomicBooleanAtomicIntegerAtomicLong
  3. -
  4. 数组:AtomicIntegerArrayAtomicLongArrayBooleanArray
  5. -
  6. 引用:AtomicReferenceAtomicMarkedReferenceAtomicStampedReference
  7. -
  8. FieldUpdaterAtomicLongFieldUpdaterAtomicIntegerFieldUpdaterAtomicReferenceFieldUpdater

AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater是FieldUpdater原子类。

  1. <a name="eabe3319"></a>
  2. ## Executors: 线程池
  3. - 接口: Executor

将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法 通常使用 Executor 而不是显式地创建线程。

  1. -
  2. ExecutorService
  3. - 继承自
  4. - 提供了什么,以及
  5. - 关闭 ExecutorService会导致
  6. -
  7. ScheduledExecutorService

ScheduledExecutorService继承自ExecutorService接口,可安排在给定的延迟后运行或定期执行的命令

  1. -
  2. AbstractExecutorService

AbstractExecutorService继承自ExecutorService接口,其提供 ExecutorService 执行方法的默认实现。此类使用 newTaskFor 返回的 RunnableFuture 实现 submit、invokeAny 和 invokeAll 方法,默认情况下,RunnableFuture 是此包中提供的 FutureTask 类。

  1. -
  2. FutureTask
  3. - 线程安全由什么来保证。

FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。

  1. -
  2. 核心: ThreadPoolExecutor
  3. -
  4. 核心: ScheduledThreadExecutor
  5. -
  6. 核心: Fork/Join框架
  7. -
  8. 工具类: Executors
  9. <a name="b7c6001d"></a>
  10. # JUC原子类: CAS, Unsafe和原子类详解
  11. - java原子类本质上使用

java原子类本质上使用的是CAS,而CAS底层是通过Unsafe类实现的

  1. <a name="CAS"></a>
  2. ## CAS
  3. -
  4. 是什么
  5. - 是一条
  6. - 实现方式
  7. - AtomicInteger类
  8. - 简单解释
  9. -
  10. CAS使用示例

java中为我们提供了AtomicInteger 原子类(底层基于CAS进行更新数据的),

  1. -
  2. CAS 问题
  3. -
  4. ABA问题
  5. -
  6. 解决思路②

变量前面追加上版本号 Atomic包里提供了一个类AtomicStampedReference来解决ABA问题 compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标 志是否等于预期标志 如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. -
  2. 循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

  1. -
  2. 只能保证一个共享变量的原子操作
  3. -
  4. Java 1.5开始

从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作

  1. <a name="aa008d20"></a>
  2. ## UnSafe类详解
  3. -
  4. 是什么

主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等 Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类

  1. ![](https://pdai.tech/_images/thread/java-thread-x-atomicinteger-unsafe.png#alt=img)
  2. -
  3. UnsafeCAS
  4. -
  5. Unsafe只提供了3CAS方法

compareAndSwapObject、compareAndSwapInt和compareAndSwapLong。都是native方法。

  1. -
  2. Unsafe底层(了解)
  3. -
  4. Unsafe其它功能

Unsafe 提供了硬件级别的操作

  1. <a name="AtomicInteger"></a>
  2. ## AtomicInteger
  3. -
  4. AtomicInteger 底层用的是
  5. -
  6. 源码解析
  7. -
  8. 原子更新基本类型

AtomicBoolean: 原子更新布尔类型。 AtomicInteger: 原子更新整型。 AtomicLong: 原子更新长整型。

  1. -
  2. 原子更新数组

AtomicIntegerArray: 原子更新整型数组里的元素。 AtomicLongArray: 原子更新长整型数组里的元素。 AtomicReferenceArray: 原子更新引用类型数组里的元素。   这三个类的最常用的方法是如下两个方法: get(int index):获取索引为index的元素值。 compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值

  1. -
  2. 原子更新引用类型
  3. - 首先
  4. - 然后
  5. - 然后

AtomicReference: 原子更新引用类型。 AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。 AtomicMarkableReferce: 原子更新带有标记位的引用类型

  1. -
  2. 原子更新字段类
  3. - 基于

AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。 AtomicLongFieldUpdater: 原子更新长整型字段的更新器。 AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。 AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。

  1. -
  2. AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束⑤

1.字段必须是volatile类型的 2.字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的 3.只能是实例变量,不能是类变量,也就是说不能加static关键字 4.只能是可修改变量,不能使final变量 5.对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

  1. <a name="58cc6f29"></a>
  2. ## AtomicStampedReference解决CAS的ABA问题
  3. -
  4. 主要维护

主要维护包含一个对象引用以及一个可以自动更新的整数”stamp”的pair对象来解决ABA问题

  1. -
  2. java中还有哪些类可以解决ABA的问题
  3. - 不是维护,而是

AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,标记值有修改,了解一下。

  1. <a name="d7fcd70c"></a>
  2. # JUC锁: LockSupport详解
  3. <a name="91a43520"></a>
  4. ## LockSupport简介

LockSupport用来创建锁和其他同步类的基本线程阻塞原语 当调用LockSupport.park时,表示当前线程将会等待,直至获得许可 当调用LockSupport.unpark时,必须把等待获得许可的线程作为参数进行传递,好让此线程继续运行

  1. <a name="29b09910"></a>
  2. ## LockSupport源码分析
  3. -
  4. 类的属性
  5. -
  6. 类的构造函数
  7. -
  8. 核心函数分析
  9. -
  10. park函数
  11. - 是什么
  12. - 下列情况发生之前都会被阻塞③
  13. - 重载版本
  14. - 有参版本其中一个setBlocker函数调用两次
  15. - 无参重载版本
  16. -
  17. unpark函数
  18. - 是什么
  19. - 安全性
  20. -
  21. parkNanos函数

禁用当前线程,并最多等待指定的等待时间 public static void parkNanos(Object blocker, long nanos) { if (nanos > 0) { // 时间大于0 // 获取当前线程 Thread t = Thread.currentThread(); // 设置Blocker setBlocker(t, blocker); // 获取许可,并设置了时间 UNSAFE.park(false, nanos); // 设置许可 setBlocker(t, null); } }

  1. -
  2. parkUntil

在指定的时限前禁用当前线程

  1. -
  2. unpark函数
  3. - 判空
  4. <a name="f87c16b4"></a>
  5. ## LockSupport示例说明
  6. -
  7. 使用wait/notify实现线程同步
  8. - 先调用notify()再调用wait
  9. ```java
  10. class MyThread extends Thread {
  11. public void run() {
  12. synchronized (this) {
  13. System.out.println("before notify");
  14. notify();
  15. System.out.println("after notify");
  16. }
  17. }
  18. }
  19. public class WaitAndNotifyDemo {
  20. public static void main(String[] args) throws InterruptedException {
  21. MyThread myThread = new MyThread();
  22. synchronized (myThread) {
  23. try {
  24. myThread.start();
  25. // 主线程睡眠3s
  26. Thread.sleep(3000);
  27. System.out.println("before wait");
  28. // 阻塞主线程
  29. myThread.wait();
  30. System.out.println("after wait");
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. }

Java 多线程与并发答案 - 图3

  • 使用park/unpark实现线程同步

    • 先调用unpark,然后调用park
    • 中断响应

比较

  • Thread.sleep()和Object.wait()的区别

  • Thread.sleep()和Condition.await()的区别

    • Object.wait()和Condition.await()原理
    • Condition.await()底层
    • 阻塞当前线程之前还干了两件事
  • Thread.sleep()和LockSupport.park()的区别

    • 唤醒方式
    • 异常
    • native方法
  • Object.wait()和LockSupport.park()的区别

    • 执行位置

    • 异常

    • 唤醒后继续执行后续内容吗?

    • wait前执行notify

    • park前执行unpark

      1. 线程不会被阻塞,直接跳过park()
  • park()/unpark()
    1. park()/unpark()底层的原理是“二元信号量”,你可以把它相像成只有一个许可证的Semaphore,只不过这个信号量在重复执行unpark()的时候也不会再增加许可证,最多只有一个许可证
  • LockSupport.park()会释放锁资源吗?
    1. 不会,park()只负责阻塞线程,释放锁资源再Conditionawait()方法.

JUC锁: 锁核心类AQS详解

AbstractQueuedSynchronizer简介

  1. AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器
  • AQS 核心思想
    1. 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
    2. 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
  • CLH队列
    1. 虚拟的双向队列,不存在队列实例
    2. AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
  • 同步状态怎么表示

  • 资源线程的排队

  • 同步状态值的修改③

    1. procted类型的getStatesetStatecompareAndSetState进行操作
  • AQS 对资源的共享方式 ```
  • 独占
    • 公平锁
    • 非公平锁
  • 共享 ```
  • 自定义同步器在实现时
    1. 自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可
  • AQS底层使用了模板方法模式

    • 自定义同步器时需要重写下面几个AQS提供的模板方法
      1. isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
      2. tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
      3. tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
      4. tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
      5. tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

AbstractQueuedSynchronizer数据结构

  • 底层的数据结构
    1. Sync queue,即同步队列,是双向链表
    2. Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表

Java 多线程与并发答案 - 图4

AbstractQueuedSynchronizer源码分析

  • 继承自,实现了,可以进行

  • AbstractOwnableSynchronizer抽象类中,可以设置②

  • AbstractQueuedSynchronizer类有两个内部类

  • 类的内部类 - Node类

    • 节点状态
      1. // CANCELLED,值为1,表示当前的线程被取消
      2. // SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
      3. // CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
      4. // PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
      5. // 值为0,表示当前节点在sync队列中,等待着获取锁
  • 类的内部类 - ConditionObject类

    • Condition接口定义了条件操作规范

      1. public interface Condition {
      2. // 等待,当前线程在接到信号或被中断之前一直处于等待状态
      3. void await() throws InterruptedException;
      4. // 等待,当前线程在接到信号之前一直处于等待状态,不响应中断
      5. void awaitUninterruptibly();
      6. //等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
      7. long awaitNanos(long nanosTimeout) throws InterruptedException;
      8. // 等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。此方法在行为上等效于: awaitNanos(unit.toNanos(time)) > 0
      9. boolean await(long time, TimeUnit unit) throws InterruptedException;
      10. // 等待,当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
      11. boolean awaitUntil(Date deadline) throws InterruptedException;
      12. // 唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
      13. void signal();
      14. // 唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。
      15. void signalAll();
      16. }
  • 类的属性
    1. 头结点head,尾结点tail,状态state、自旋时间spinForTimeoutThreshold
    2. AbstractQueuedSynchronizer抽象的属性在内存中的偏移地址
  • 类的构造方法

  • 类的核心方法 - acquire方法

    • 以什么方式获取资源,中断怎么处理
      Java 多线程与并发答案 - 图5

    • acquireQueued方法的整个的逻辑

      1. 1.判断结点的前驱是否为head并且是否成功获取(资源)。
      2. 2.若步骤1均满足,则设置结点为head,之后会判断是否finally模块,然后返回。
      3. 3.若步骤2不满足,则判断是否需要park当前线程,是否需要park当前线程的逻辑是判断结点的前驱结点的状态是否为SIGNAL,若是,则park当前结点,否则,不进行park操作。
      4. 4.park了当前线程,之后某个线程对本线程unpark后,并且本线程也获得机会运行。那么,将会继续进行步骤①的判断。
  • 类的核心方法 - release方法

AbstractQueuedSynchronizer总结

  1. - 每一个结点都是由前一个结点唤醒
  2. - 当结点发现前驱结点是head并且尝试获取成功,则会轮到该线程运行
  3. - condition queue中的结点向sync queue中转移是通过signal操作完成的
  4. - 当结点的状态为SIGNAL时,表示后面的结点需要运行

JUC锁: ReentrantLock详解

ReentrantLock源码分析

  • 类的继承关系

    • 实现了
  • 类的内部类③

Java 多线程与并发答案 - 图6

  • 类的属性
  • 类的构造函数②

JUC锁: ReentrantReadWriteLock详解

ReentrantReadWriteLock数据结构

  • 底层是基于什么来实现的
    1. ReentrantLockAbstractQueuedSynchronizer

ReentrantReadWriteLock源码分析

  • 实现了什么接口②
    1. 实现了ReadWriteLock接口
  • 类的内部类⑤

  • 内部类 - Sync类

    • 类的继承关系

    • 类的内部类② ``` HoldCounter和ThreadLocalHoldCounter HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程

    ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是initialValue方法里面生成的那个HolderCounter对象 ```

  • 类的属性

  • 类的构造函数

  • 内部类 - Sync核心函数分析

    • sharedCount函数
    • exclusiveCount函数
    • tryRelease函数
    • tryAcquire函数
  • 类的属性

JUC集合: ConcurrentHashMap详解

为什么HashTable慢

  1. 使用了synchronized关键字对put等操作进行加锁
  2. synchronized关键字加锁是对整个对象进行加锁
  3. 也就是说在进行put等修改Hash表的操作时,锁住了整个Hash

ConcurrentHashMap - JDK 1.7

  • 数据结构
    1. ConcurrentHashMap 是一个 Segment 数组
    2. Segment 通过继承 ReentrantLock 来进行加锁
  • 初始化

    • initialCapacity: 初始容量
      1. 这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment
  • loadFactor: 负载因子
    1. Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用
  • Segment 数组长度,且

  • Segment[i] 的默认大小为 ,负载因子是 ,得出初始阈值为

  • 只初始化了

  • put 过程分析

  • 初始化槽: ensureSegment

  • 获取写入锁: scanAndLockForPut

  • 扩容: rehash

  • get 过程分析

  • 并发问题分析

ConcurrentHashMap - JDK 1.8

  • 加锁采用
    1. 加锁则采用CASsynchronized实现
  • 数据结构

    • 初始化
      1. 通过提供初始容量,计算了 sizeCtl
      2. sizeCtl = (1.5 * initialCapacity + 1),然后向上取最近的 2 n 次方】
  • putVal流程
    1. 1.判断key value 不为空
    2. 2.计算hash
    3. 3.根据对应位置的节点的类型来赋值,或者helpTransfer,或者增长链表,或者给红黑树增加节点
    4. 4.检查满足阈值就"红黑树化"
    5. 5.返回oldVal
  • get流程
    1. 1.计算hash
    2. 2.找到对应的位置,根据情况进行:
    3. 直接取值
    4. 红黑树里找值
    5. 遍历链表取值
    6. 返回找到的结果

JUC集合: CopyOnWriteArrayList详解

CopyOnWriteArrayList源码分析

  • 类的继承关系④
    1. CopyOnWriteArrayList实现了List接口,List接口定义了对列表的基本操作;同时实现了RandomAccess接口,表示可以随机访问(数组具有随机访问的特性);同时实现了Cloneable接口,表示可克隆;同时也实现了Serializable接口,表示可被序列化。
  • 类的内部类

    • COWIterator类
  • 类的属性

JUC集合: ConcurrentLinkedQueue详解

ConcurrentLinkedQueue数据结构

ConcurrentLinkedQueue源码分析

  • 类的继承关系

JUC集合: BlockingQueue详解

JUC线程池: FutureTask详解

FutureTask简介

  • 常用来封装,也可以
    1. FutureTask 常用来封装 Callable Runnable,也可以作为一个任务提交到线程池中执行
  • FutureTask 的线程安全由什么来保证
    1. FutureTask 的线程安全由CAS来保证

FutureTask类关系

Java 多线程与并发答案 - 图7

FutureTask源码解析

  • Callable接口
  • Future接口

JUC线程池: ThreadPoolExecutor详解

为什么要有线程池③