• 第03篇_并发编程

    第01章_多线程开发

    第一节 线程基础

    1. 线程简介

    1) 什么是线程?

    线程CPU调度和执行的最小单位,线程之间共享进程的方法区(元空间),但是有各自的方法栈程序计数器等,适合任务协作和高并发性能的场景。

    注意:

    1. 进程是操作系统分配资源的最小单位,在启动Java应用时,就是启动了一个JVM进程,它包括主线程(main)和其它线程。

     

    2) 线程的六大状态

    在 Java 中,线程状态被定义为枚举类:Thread.State,有如下六种状态(JVM不区分Running和Ready)

    image-20260204225300053

    线程状态状态说明
    NEW新建状态,线程创建但未启动。
    RUNNABLE运行状态/就绪状态,线程正在运行,或在等待操作系统调度。
    BLOCKED被动阻塞状态,在等待锁或IO等资源。
    WAITING主动等待状态,线程在等待某个条件,调用wait()/join()方法会进入该状态。
    TIMED_WAITING主动超时等待状态,线程在等待某个条件或超时,调用wait(timeout)/join(millis)/sleep(millis)会进入该状态。
    TERMINATED终止状态,线程已结束。

    注意:

    1. RUNNABLE不代表CPU一定在执行该线程的代码,也可能在等待操作系统分配时间片,只是它没有在等待其他条件。
    2. 在操作系统中,其实只有五种状态,其中Running根据是否在运行拆分出就绪态Ready,但三个阻塞/等待状态不做区分。

     

     

    2. 创建和启动线程

    1) 直接创建Thread

    可以通过继承java.lang.Thread类,重写其run方法来直接定义一个线程。

    注意:

    1. 此种方式存在单继承问题,不推荐使用。

     

    2) 定义Runable实现类

    由于Java只支持单继承,因此一般都是定义java.lang.Runnable接口的实现类,然后创建Runnable的对象传给Thread对象去执行。

    注意:

    1. 一定要通过 start 方法启动线程,如果使用 run 方法启动,将会在当前线程执行。

     

    3) 定义Callable+FutureTask实现类

    java.lang.Callable一般用于有返回结果同步非阻塞的执行方法,需要使用FutureTask包装后执行:

     

     

    3. 线程的相关操作

    1) 线程的基本属性

     

    2) 线程的常用方法

    注意:

    1. wait()对象级别的方法,定义在 Object 类,用于线程间的协作,需要在同步代码块中调用,会释放对象锁
    2. join()线程级别的方法,定义在 Thread 类,用于线程的同步,不会释放锁,用于等待目标线程执行完成。

     

     

    4. 线程的结束方式

    1) 正常/异常返回

    正常/异常返回就是让线程的run方法结束,无论是return结束,还是抛出异常结束,都可以。

    一般而言,对于以线程提供服务的程序模块,它应该封装取消或关闭的操作,让调用者能正确关闭线程

     

    2) 调用stop强制停止

    强制让线程结束,无论你在干嘛,不推荐使用此种方式,但是,他确实可以把线程干掉。

     

    3) 线程中断

    线程中断是一种协作机制,通过给线程传递一个取消信号,由线程来决定如何以及何时退出,并不是强迫终止一个线程。

    每个线程都有一个中断标志位,表示该线程是否被中断,可通过如下方式获取和设置:

    线程处于不同状态时,对中断方法的响应也会有所区别:

     

     

    5. 面试扩展

    1) 并行 vs 并发

     

    2) 阻塞 vs 非阻塞

    主要取决于等待时能做别的吗,阻塞是 “等待时躺平”,资源被占用且无法复用,非阻塞是 “等待时摸鱼”,资源可高效利用:

     

    3) 同步 vs 异步

    主要取决于任务结果怎么通知,同步是 “主动等结果”,全程不脱离任务流程,异步是 “被动收通知”,发起后即可脱离任务流程:

    同步非阻塞与异步非阻塞的区别?

    答:非阻塞指调用后立即返回,无需等待,同步还是异步取决于主动检查结果还是通过回调处理结果。

     

     

    第二节 线程同步

    1. 线程并发问题

    1) 有序性

    有序性指令的执行顺序编写顺序是否一致,在JIT编译CPU执行层面,都可能会对指令进行重排序操作,以优化执行效率。

     

    2) 可见性

    可见性指一个线程对共享变量做了修改,其它线程是否可以立即看到修改后的值。

    注意:

    1. 在 Intel CPU 缓存层面,有一个MESI协议,标识缓存的修改状态,可以保证CPU缓存一致性问题。
    2. 但注意写缓冲器无效化队列会对其进行破坏,部分情形需结合性能稍低的总线锁来保证。
    3. 伪共享:CPU缓存是按行加载的,如果不同CPU加载了同一个缓存行,分别对该行的X和Y变量进行修改,会导致缓存频繁失效。

     

    3) 原子性

    原子性指多次操作要么全都执行,要么全都不执行,不会受任何因素而中断。

     

     

    2. synchronized

    1) synchronized简介

    synchronized是 Java 内置的 线程同步关键字,用于保证多线程环境下共享资源的安全访问,核心特性如下:

    注意:

    1. 在等待synchronized锁时,进入锁对象的锁等待队列,线程状态为BLOCK状态,此时无法响应中断

     

    2) 基本使用
    修饰实例方法

    修饰实例方法,对 this对象 进行加锁:

    注意:

    1. 特别注意,同一个对象的不同synchronized方法不能同时执行,但不同对象的同一synchronized方法能够同时执行。
    2. 无法修饰构造方法,因为不可能多个线程同时调用同一个对象的构造方法,如需加锁,需在内部使用静态代码块。

     

    修饰静态方法

    修饰静态方法,对 类对象(StaticCounter.class) 进行加锁:

    注意:

    1. 同一个对象的静态synchronized方法和实例synchronized方法修饰的是不同对象,可以同时执行。

     

    修饰代码块

    修饰代码块,对 指定对象 进行加锁:

    注意:

    1. 指定的对象不能为null,否则会抛 NullPointerException。
    2. 尽量不要使用包装类字符串作为锁对象,因为它们有缓存池或常量池,锁对象粒度会过大。

     

    3) 实现原理

    在Java的对象头中,有一个64位MarkWord字段,用于记录 synchronized锁的四种状态变化:

    image-20251208204349054

    在升级到重量级锁后,调用的是C++编写的ObjectMonitor.hpp代码:https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.cpp

     

    4) 扩展知识

     

     

    3. wait/notify

    1) wait/notify简介

    在Java中,任意对象都可以参与线程协作,它们都有waitnotify方法,可以在synchronized代码块中调用:

    特别注意,从wait返回后,不一定表示等待的条件就满足了,仍需进行条件检查,因此,wait方法的一般调用模式为:

    注意:

    1. 并非调用notify方法后就立即释放锁,而是等到当前synchronized代码块执行完毕后才会释放,其它线程才可能获取到锁。
    2. wait/notify 机制只支持一个条件队列,如果有多个等待条件,只能共用,并且在通知时必须通知所有等待的线程
    3. 线程对象的join方法是基于wait方法实现的,当子线程运行结束时,由 JVM 调用notifyAll来通知。

     

    2) 生产者/消费者模型

    生产者线程和消费者线程通过共享队列进行协作,生产者将数据或任务放到队列上,而消费者从队列上取数据或任务。如果队列长度有限,在队列满的时候,生产者需要等待,而在队列为空的时候,消费者需要等待。

    它们协作的共享变量是队列,我们将队列作为单独的类进行设计,代码如下:

    在上面代码中,生产者和消费者都调用了wait方法,但它们等待的条件是不一样的。生产者在队列为满时等待,而消费者在队列为空的时候等待,它们等待条件不同但又使用相同的条件队列,所以要调用notifyAll而不能调用notify,因为notify可能唤醒的恰好是同类线程。

    类似的,它们也都调用了notifyAll方法,但是需满足的条件也不一致,生产者在“有数据了”的条件下通知消费者,消费者在“有空位了”的条件下通知生产者。

     

    3) 同时开始模型

    在运动员比赛中,听到比赛开始枪响后同时开始,在一些程序,尤其是模拟仿真程序中,要求多个线程能同时开始。

    它们协作的共享变量是一个开始信号,我们用一个类FireFlag来表示这个协作对象:

    子线程应该调用waitForFire()等待枪响,而主线程应该调用fire()发射比赛开始信号,代码如下:

     

    4) 等待结束模型

    主线程将任务分解为若干个子任务,为每个子任务创建一个线程,主线程在继续执行其他任务之前需要等待每个子任务执行完毕。

    主线程与各个子线程协作的共享变量是一个数,这个数表示未结束线程个数,代码如下:

    应用代码示例如下:

    注意:

    1. 可将线程计数初始值设置为1,由子线程调用await(),主线程调用countDown(),还可实现上面的“同时开始”模式。

     

    4) 集合点模型

    在并行迭代计算中,每个线程负责一部分计算,然后在集合点等待其他线程完成,所有线程到齐后,交换数据和计算结果,再进行下一次迭代。

    它们协作的共享变量依然是一个数,这个数表示未到集合点的线程个数。

    多个游客线程,各自先独立运行,然后使用该协作对象到达集合点进行同步的示例代码如下:

     

    5) 非阻塞调用模型

    在并发编程中,一种常见的模式是将子线程的管理封装为非阻塞调用,非阻塞调用马上返回,但返回的不是最终的结果,而是一个一般称为Promise或Future的对象,通过它可以在随后获得最终的结果。

    异步结果模式依赖异步调用框架,主要由调用者执行器异步任务异步结果四个部分组成。

    其中异步任务和异步结果代码表示如下:

    执行器用于执行子任务并返回异步结果,使用执行器后调用者就无需创建并管理子线程了,其代码如下:

    调用者只需创建执行器,然后执行异步任务,即可得到异步结果对象。

     

     

    4. 面试扩展

    1) JMM简介

    JMM是一个语言层面的内存模型抽象,用于描述多线程访问共享变量时的一系列行为规范。主要由两部分组成:

    交互原则:变量都存储在主内存中,在操作时,需从主内存复制一份副本到本地内存,修改完毕后,再写回主内存。

    交互指令:lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)。

    image-20251208200935925

    注意:

    1. JMM是抽象的,定义了线程与主内存、本地内存之间的交互规范,Java运行时内存区域是具体的,定义了运行时内存的划分。

     

    2) sleep/wait/join/park的区别

     

    3) CAS/syncchronized/Lock的对比
    对比维度CAS 的核心特点synchronized 的核心特点Lock 的核心特点
    实现原理无锁,依赖 CPU 原子指令(如 cmpxchg),无需阻塞线程基于对象监视器锁(Monitor),JVM 底层实现,可阻塞线程基于 AQS(抽象队列同步器),API 层面实现,可阻塞线程
    锁类型无锁(非阻塞同步),不存在 “锁” 的概念悲观锁,支持可重入,默认非公平锁悲观锁,支持可重入,可选公平 / 非公平锁
    核心特性无线程阻塞,但有 ABA 问题、自旋消耗自动释放锁(异常 / 方法结束),无扩展特性支持超时获取、可中断、条件变量(Condition),特性丰富
    使用场景简单原子操作(如原子类 AtomicInteger),追求极致性能通用并发场景,代码简洁,无需手动处理锁释放复杂并发场景(如需要中断等待、超时获取锁),需灵活控制锁

     

    4) 悲观锁 vs 乐观锁

    注意:

    1. 乐观锁无需切换上下文及内核态,在并发量低时,性能表现更好,可通过 CAS 来实现。

     

    5) 关于死锁

    以不同顺序对多个资源进行加锁时,就可能造成死锁,即持有锁A的同时去获取锁B,但锁B已被其它线程持有且恰好需要获取锁A。

    死锁检测:可通过jstack -pid命令查看。

    避免死锁:

     

    6) 线程安全问题有哪些解决措施

    线程表示一条单独的执行流,有各自的计数器和栈等,但是内存是共享的,并发操作就会存在竞争内存可见性问题,解决思路如下:

     

    7) 线程之间的协作机制有哪些?

    线程之间需要相互协作,来解决业务问题:

     

     

    第三节 线程本地变量

    1. 基本使用

    1) ThreadLocal简介

    线程本地变量(ThreadLocal)指与线程绑定的变量,即每个线程都有同一个变量的独有拷贝,在Java中,用ThreadLocal表示。

    注意:

    1. Thread内部存在一个 inheritableThreadLocals 变量,可用于跨线程传递 ThreadLocal 的值,但不支持线程池场景。
    2. 阿里开源工具类可以解决上述问题:TransmittableThreadLocal,它自定义了线程类并对线程池进行了装饰。

     

    2) 应用示例

    注意:

    1. 一般来说,ThreadLocal对象都定义为static,以便于引用。

     

     

    2. 源码分析

    1) 实现思路

    Thread 类内部,有一个名为 threadLocalsThreadLocalMap 对象,其 Key 为 ThreadLocal 对象的弱引用,Value 为该变量在该线程的值,在调用 ThreadLocal 的 get/set 方法时,就是获取当前线程的该变量进行 get/set,而 ThreadLocal 的作用更像是一个工具类

     

    2) set方法实现

    每个线程都有一个ThreadLocalMap,调用set实际上是在线程自己的Map里设置了一个条目,键为当前的ThreadLocal对象,值为value。

    注意:

    1. 当前线程对象不能持有ThreadLocal对象的强引用,否则必须等到当前线程结束,ThreadLocal对象才能被GC回收,所以ThreadLocalMap 的 Key 采用弱引用

     

    3) get方法实现

     

    4) remove方法实现

    注意:

    1. 在使用完 ThreadLocal 变量后,必须调用 remove() 方法清空 ThreadLocalMap 中的相关条目,否则业务数据(value)一直被当前线程持有,无法被GC回收,可能导致内存泄漏。

     

    3. 面试扩展

    1) 在线程池中使用TL时如何清理?

    如果在线程池任务中使用了ThreadLocal,并且未进行清理操作,那么会将修改后的值带到下一个任务,清理方式如下:

     

     

    第02章_并发工具

    第一节 CAS工具

    1. volatile

    1) volatile简介

    volatile关键字可修饰变量,表示该变量是共享且不稳定的,禁用 CPU 缓存,每次都从主内存读取,防止出现内存可见性问题

    另外,还会通过插入特定的 内存屏障 的方式来防止 JVM 的指令重排序,但它不能保证对变量的操作是原子性的。

     

     

    2. CAS

    1) CAS简介

    CAS 是 Compare and Swap(比较并交换) 的缩写,是一种无锁的原子操作,在Java中,是通过Unsafe 类调用 CPU 指令实现的。

     

    2) CAS优缺点

    优点:

    缺点:

     

     

    3. 原子变量

    1) 原子变量简介

    在Java中,基于CAS在应用层提供了一系列的原子变量,可以保证更新操作的原子性,且无需加锁以及上下文切换,效率更高。

    基本类型原子变量原子数组原子字段更新器
    BooleanAtomicBoolean  
    IntegerAtomicIntegerAtomicIntegerArrayAtomicIntegerFieldUpdater
    LongAtomicLongAtomicLongArrayAtomicLongFieldUpdater
    ReferenceAtomicReferenceAtomicReferenceArrayAtomicReferenceFieldUpdater

    注意:

    1. 在不同CPU架构中,CAS的指令也不相同,在 x86 架构中,CAS指令为: cmpxchg

     

    2) AtomicInteger

    AtomicInteger可以用作计数器等场景,常用方法如下:

    AtomicBoolean可以用来在程序中表示一个标志位,它的原子操作方法有:

    AtomicLong可以用来在程序中生成唯一序列号,它的方法与AtomicInteger类似。

    注意:

    1. 在实际应用中,更多的是使用LongAddr作为并发计数器,性能更高。

     

    3) AtomicReference

    AtomicReference用来以原子方式更新引用类型,它有一个类型参数,使用时需要指定引用的类型。以下代码演示了其基本用法:

     

    4) AtomicArray

    原子数组方便以原子的方式更新数组中的每个元素,我们以AtomicIntegerArray为例来简要介绍下。

     

    5) FieldUpdater

    FieldUpdater方便以原子方式更新对象中的字段,字段不需要声明为原子变量,FieldUpdater是基于反射机制实现的,看代码:

     

     

    第二节 AQS工具

    1. AQS

    1) AQS简介

    AbstractQueuedSynchronizer是Java提供的一个抽象类,它封装了CAS和LockSupport,简化了并发工具的实现。原理简述如下:

     

    2) LockSupport

    java.util.concurrent.locks.LockSupport线程阻塞与唤醒的底层工具类,无需持有锁、唤醒精准、支持预存许可,解决了 Object.wait ()/notify () 的诸多限制,是 AQS、ReentrantLock 等同步工具的 “基石”。

    注意:

    1. 与CAS方法类似,park/unpark也是通过Unsafe类间接调用操作系统API来实现的。

     

     

    2. 可重入锁(ReentrantLock)

    1) Lock接口

    java.util.concurrent.locks.Lock显式锁的顶层接口,定义了锁的获取释放中断等核心操作。

    注意:

    1. 相比于synchronized关键字,显式锁支持中断响应超时唤醒尝试获取公平锁等,使用时更加灵活。

     

    2) 基本使用

    ReentrantLock是显示锁接口的主要实现类,常用方法如下:

    使用示例如下:

    注意:

    1. 在获取多个锁时,如果无法确定以相同的顺序获取,则可通过tryLock尝试,如果获取不到则释放已持有的锁,避免死锁。

     

    3) lock方法实现

    image-20251208210709847

    我们先来看下ReentrantLock的lock方法,整体流程图如下:

    image-20251122175749027

    其中尝试获取锁的方法 tryAcquire 必须被子类重写,NonfairSync的实现如下:

    如果 tryAcquire 返回false,即被其它线程锁定,则AQS会调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg),其中addWaiter会新建一个节点Node,代表当前线程,然后加入到内部的锁等待队列中。

    放入锁等待队列中后,再调用acquireQueued尝试获得锁,代码为:

    以上就是lock方法的基本过程,能获得锁就立即获得,否则加入等待队列,被唤醒后检查自己是否是第一个等待的线程,如果是且能获得锁,则返回,否则继续等待,这个过程中如果发生了中断,lock会记录中断标志位,但不会提前返回或抛出异常。

     

    4) tryLock方法实现

     

    5) tryLock(time,unit)方法实现

     

    6) cancelAcquire方法实现

    image-20251208210639183

     

    7) lockInterruptibly方法实现

     

    8) unlock方法实现

    image.png

    下面是ReentrantLock的unlock方法的实现代码,整理流程图如下:

    image-20251122180629733

     

     

     

     

    3. 条件变量(ConditionObject)

    1) ConditionObject简介

    显式条件基于显式锁的 await/signal 线程协作机制,它们之间的关系类似于 synchronized 与 wait/notify 协作机制的关系。

    创建显式条件需要通过显式锁,Lock接口定义了创建方法:

    其中Condition为显式条件的接口,它的定义为:

    与wait/notify协作机制类似,await()/signal()也具有如下一些特性:

     

    2) 使用示例(生产者/消费者模型)

    生产者/消费者模式存在一个与队列有关的条件,还存在一个与队列有关的条件,而在前面通过wait/notify机制实现时,不得不共用同一个条件队列,而使用显式锁,则可以分别创建对应的条件队列。

    这样,代码更为清晰易读,同时避免了不必要的唤醒和检查,提高了效率。

     

    3) await方法实现

    ConditionObject是AQS中定义的一个成员内部类,它可以直接访问AQS中的数据,比如AQS中定义的锁等待队列。它通过显式锁创建:

    它内部也有一个条件等待队列,其成员声明为:

    await方法实现分析如下:

     

    4) awaitNanos实现分析

    awaitNanos与await的实现是基本类似的,区别主要是会限定等待的时间,如下所示:

     

    5) signal实现分析

     

     

    4. 读写锁(ReentrantReadWriteLock)

    1) ReentrantReadWriteLock简介

    在Java并发包中,接口ReadWriteLock表示读写锁,主要实现类是可重入读写锁ReentrantReadWriteLock

    ReentrantReadWriteLock也是基于AQS实现的,也是对state进行操作,拿到锁资源就去干活,如果没有拿到,就去AQS队列中排队。

     

    2) 应用示例

     

    3) 写锁加锁源码

    image-20251208212117654

     

    4) 写锁释放源码

    释放的流程和ReentrantLock一致,只是在判断释放是否干净时,判断低16位的值。

     

    5)读锁加锁源码

    image-20251208212526261

     

    6) 读锁重入源码

     

    7) 读锁释放源码

     

    8) 扩展知识

    StampedLock :邮戳锁,是对 ReentrantReadWriteLock 的改进,额外支持乐观读,且性能更好,写线程更容易获得锁,但它不支持重入条件变量,代码也相对复杂。

     

     

    5. 信号量(Semaphore)

    1) Semaphore简介

    Semaphore可以通过控制许可数量来限制同时访问特定资源的线程数,支持许可的获取与释放,可实现并发限流。

     

    2) 基本使用

    注意:

    1. Semaphore是不可重入的,即使在同一个线程,每一次的acquire调用都会消耗一个许可。
    2. 一般锁只能由持有锁的线程释放,而Semaphore表示的只是一个许可数,任意线程都可以调用其release方法
    3. 因此,即使将permits设置为1,它和一般的锁还是有本质的不同。

     

    3) 核心属性&构造

    Semaphore的内部类Sync继承了AQS,是一个基于AQS实现的计数器,计数器值存储在AQS的state变量,当申请许可时计数减少,当释放许可时,计数增加,构造方法如下:

     

    6. 倒计时门栓(CountDownLatch)

    1) CountDownLatch简介

    CountDownLatch可以让一组线程等待另一组线程完成后再继续执行,本质上是一个计数器,在构造时指定初始值,只能计减,在减为0后,唤醒所有等待的线程。应用场景如下:

     

    2) 基本使用

    前面介绍过门栓的两种应用场景,一种是同时开始,另一种是等待结束,它们都有两类线程,互相需要同步,实现代码如下:

    注意:

    1. countDown的调用应该放到finally语句中,确保在工作线程发生异常的情况下也会被调用,使主线程能够从await调用中返回。

     

    3) 核心属性&构造

    CountDownLatch的内部类Sync继承了AQS,也是一个基于AQS实现的计数器,计数器值存储在AQS的state变量,构造方法如下:

     

    4) await方法实现

    await方法就是判断当前CountDownLatch中的state是否为0,如果为0,直接正常执行后续任务;

    如果不为0,以共享锁的方式,插入到AQS的双向链表,并且挂起线程。

     

    5) countDown方法实现

    countDown方法本质就是对state - 1,如果state - 1后变为0,需要去AQS的链表中唤醒挂起的节点。

     

     

    7. 循环栅栏(CyclicBarrier)

    1)CyclicBarrier简介

    CyclicBarrier可以让一组线程相互等待至全部到达屏障点后再共同执行,屏障可自动重置并重复使用,还可配置触发回调任务。应用场景如下:

    注意:

    1. 只要有一个线程抛出BrokenBarrierException,就会导致所有在调用await的线程都抛出BrokenBarrierException。
    2. 此外,如果栅栏动作(集合点动作)抛出了异常,也会破坏栅栏。

     

    2) 基本使用

     

    3) 核心属性&构造

    CyclicBarrier并没有直接基于AQS,而是基于ReentrantLock实现对屏障点的计减以及线程挂起操作。

     

    4) await方法

    在CyclicBarrier中,提供了2个await方法:

    无论是哪种await方法,核心都在于内部调用的dowait方法,该方法主要包含了线程互相等待的逻辑,以及屏障点数值到达0之后的操作

     

     

    8. 面试扩展

    1) CAS和AQS的区别?

     

    2) 公平锁 vs 非公平锁?

     

    3) Lock vs synchronized

     

    4) ReentrantLock vs synchronized

     

    5) AQS为啥需要一个head节点?

    在AQS的双向锁等待队列中,head节点不是必须的,但是可以对唤醒后续节点的操作进行优化。即通过判断head节点的等待状态是否为-1,得出是否有节点需要被唤醒,如果有,采取遍历双向链表。

     

    6) AQS中为什么使用双向链表?

    主要是为了更方便的操作Node节点,如取消节点时,如果有前指针,则可以很方便的将前节点的next节点执行当前节点的next节点。

     

    7) ReentrantLock 的 lock 和 lockInterruptibly 有什么区别?

    两者均会阻塞获取锁,核心区别是中断响应

    实际使用中,需要线程取消或超时控制时用 lockInterruptibly,无需中断响应时用 lock。

     

    8) CyclicBarrier vs CountDownLatch

    CyclicBarrier与CountDownLatch可能容易混淆,它们主要有两点区别:

     

    第三节 阻塞队列

    1. 阻塞队列简介

    1) BlockingQueue

    java.util.concurrentBlockingQueue阻塞队列的顶层接口,主要方法如下:

     

    2) 常用实现类

    BlockingQueue的常见实现类如下,这些队列大都是基于ReentrantLock+Condition实现的,是并发安全的弱一致性的

    队列类型队列类名
    普通阻塞队列ArrayBlockingQueueLinkedBlockingQueue、LinkedBlockingDeque
    优先级阻塞队列PriorityBlockingQueue
    延时阻塞队列DelayQueue
    其他阻塞队列SynchronousQueue:实时队列,容量为0,必须实时传递
    LinkedTransferQueue:传递队列,容量无界,支持实时传递,也可以存储数据

     

     

    2. ArrayBlockingQueue

    1) 基本使用

    ArrayBlockingQueue 是基于数组实现的有界阻塞队列,需在初始化时指定容量,不可扩容,适用于固定大小的缓冲池等场景。

     

    2) 核心属性

    ArrayBlockingQueue的底层用ReentrantLock保证线程安全,入队 / 出队共用一把锁,高并发下性能一般。

     

    3) 生产者方法实现
    add

    add方法本身就是调用了offer方法,如果offer方法返回false,直接抛出异常

     

    offer

     

    offer(time,unit)

    生产者在添加数据时,如果队列已经满了,会阻塞一会。

     

    put

    如果队列是满的, 就一直挂起,直到被唤醒,或者被中断:

     

     

    4) 消费者方法实现
    remove

     

    poll

     

    poll(time,unit)

     

    take

     

     

    3. LinkedBlockingQueue

    1) 基本使用

    LinkedBlockingQueue是基于链表实现的无界阻塞队列,适用于任务数量不确定,追求高并发性能的场景,如线程池的任务队列。

     

    2) 核心属性

    LinkedBlockingQueue 在入队和出队时使用两把独立的锁(入队锁、出队锁),高并发下性能比 ArrayBlockingQueue 好。

     

    3) 生产者方法实现
    add

     

    offer

     

    offer(time,unit)

     

    put

     

    4) 消费者方法实现
    put

     

    poll

     

    poll(time,unit)

     

    take

     

     

    4. PriorityBlockingQueue

    1) 基本使用

    PriorityBlockingQueue是基于小顶堆实现的无界阻塞队列,适用于按优先级处理任务的场景,如任务调度系统等。

    注意:

    1. PriorityBlockingQueue队列要求元素需实现Comparable接口或在构造时指定Comparator

     

    2) 核心属性

     

    3) 生产者方法实现
    offer

     

    tryGrow(扩容)

     

    siftUpComparable(上浮)

     

     

    4) 消费者方法实现
    poll

     

    poll(time,unit)

     

    take

     

    siftDownComparable(下移平衡二叉堆)

     

     

    5. DelayQueue

    1) 基本使用

    DelayQueue是基于优先级队列实现的无界阻塞队列,存入的元素必须在延时到期后才能被取出,适用于 定时任务等场景。

    注意:

    1. DelayQueue要求元素必须实现Delayed接口,指定延迟时间。

     

    2) 核心属性

     

    3) 生产者方法实现(略)

     

    4) 消费者方法实现(略)

     

     

    6. SynchronousQueue

    1) 基本使用

    SynchronousQueue(实时队列)容量为0的特殊阻塞队列,入队操作(put)必须等待对应的出队操作(take),适用于线程间直接传递数据的场景,是CachedThreadPool的默认队列。

     

    2) 核心属性

     

    3) 生产者方法实现(略)

     

    4) 消费者方法实现(略)

     

    7. Disruptor

     

     

     

    第四节 并发容器

    1. CopyOnWriteList

    1) 基本使用

    CopyOnWriteArrayList 是一个线程安全的 ArrayList ,是基于写时复制机制(Copy On Write,简称 COW)实现的。

    注意:

    1. JUC还提供了一个CopyOnWriteArraySet,基于在CopyOnWriteArrayList的复合操作addIfAbsent实现的。

     

    2) 核心属性&构造

     

    2) 读操作(get)

    读操作就是基于数组索引位置获取数据,始终读取的是原数组,不需要加锁。

     

    3) 写操作(add)

    CopyOnWriteArrayList是基于lock锁和副本数组的形式保证线程安全。

     

    4) 删除操作(remove)

    删除操作分为两种,一种是基于数组索引进行删除,另一种是基于元素值删除查找到的第一个元素。

     

    5) 修改操作(set)

     

    6) 清空操作(clear)

     

    7) 迭代操作

    用ArrayList时,如果想在遍历的过程中去移除或者修改元素,必须使用迭代器才可以。

    但是CopyOnWriteArrayList中即便用了迭代器也不让做写操作,因为不希望迭代时,还需要加锁。

     

     

    2. ConcurrentHashMap

    1) 基本使用

    ConcurrentHashMap是一个线程安全高效的HashMap,读操作完全并行,写操作支持一定程度的并行

    注意:

    1. 可以使用Collections.newSetFromMap方法基于ConcurrentHashMap构建一个线程安全的HashSet
    2. HashMap在在多个线程同时扩容哈希表的时候,链表结构可能形成环,出现死循环,占满CPU。

     

    2) 存储结构

    在JDK1.8中,ConcurrentHashMap的存储结构为数组+链表+红黑树

    image-20251210192902678

     

    3) 核心属性

    注意:

    1. 在JDK1.7中,ConcurrentHashMap基于分段锁实现,核心属性有所不同。

     

    4) 写入操作流程(put)

     

    5) 扩容流程

    ConcurrentHashMap扩容操作只有两个事情:

    区分第一个来扩容和协助扩容的方式:

    为了区分谁是第一个来扩容的线程,谁是来协助扩容的线程,需要掌握一个属性,这个属性就是sizeCtl

    第一个来扩容的线程会优先修改sizeCtl的值,将其修改为一个<-1的值。

    其他所有来协助扩容的线程,发现sizeCtl<-1,直接对sizeCtl做 + 1操作,代表我来扩容了。

    修改sizeCtl的操作,是基于CAS完成的。

    第一个来扩容的线程,由他去初始化新数组,并且做迁移数据的操作。协助扩容的线程,就是来帮忙将老数组的数据迁移到新数组的。


    整体扩容的流程(以第一个来扩容的线程)

     

    6) 查询操作流程(get)

    ConcurrentHashMap本来就是作为JVM缓存使用的,对查询速度要求极高。所以在ConcurrentHashMap中 查询操作永远不会阻塞 。无论什么情况,都能去查数据。

    查询数据的操作无非就是几种情况:

    lockState == 1:有写线程正在平衡红黑树。

    lockState == 2:代表写线程在等着平衡红黑树。

    lockState >= 4:代表有读线程在读取红黑树中的数据。(读线程来读数据,就对lockState + 4)

    红黑树是一个平衡的二叉树,怎么保持平衡的?

    为了保证平衡,写操作可能会旋转某个节点,导致节点的指针发生变化。如果此时读线程在红黑树中遍历找数据,结果写线程改变了红黑树的指针,导致无法找到对应的数据。

    系统讲解~~~

     

    3. ConcurrentSkipListMap

    1) SkipList

    SkipList称为跳表跳跃表,是一种基于有序链表多级索引数据结构,使其更易于实现高效并发算法。

    下面是一个包含3, 6, 7, 9, 12, 17, 19, 21, 25, 26元素的跳表结构,两条线展示了查找值19和8的过程:

    img

     

    2) ConcurrentSkipListMap

    ConcurrentSkipListMap实现了ConcurrentMapSortedMap等接口,是线程安全的TreeMap,要求Key可比较或传入比较器。

    注意:

    1. ConcurrentSkipListMap的size方法不是常量操作,需要遍历所有元素,且遍历结束后size可能已改变,因此一般用处不大。
    2. TreeSet是基于TreeMap实现的,与此类似,也提供了基于ConcurrentSkipListMap的ConcurrentSkipListSet

     

     

    4. ConcurrentLinkedQueue

    1) 基本使用

    ConcurrentLinkedQueue是一个线程安全无锁的单向链表实现,是LinkedList的线程安全版本。

     

    2) 实现原理

    直接基于CAS原子操作+自旋来保证并发安全,核心结构是带有 head(头节点)和 tail(尾节点)的单向链表:

    image-20251210194026538

     

     

    5. synchronizedCollection

    1) 基本使用

    Collections类可对部分普通容器进行修饰,对每个容器方法都加上synchronized,使其具备线程安全性:

    注意:

    1. 在并发访问量比较大时,同步容器的性能较差,推荐使用Concurrent系列的并发容器,性能更高且支持复合操作。

     

    2) 复合操作

    在基于同步容器的方法进行复合操作时,不能保证原子性:

    如需解决上述问题,可在putIfAbsent方法中使用synchronized代码块,并且使用map进行加锁,这样锁对象就是同一个了。

     

    3) 并发修改异常

    增删元素的同时进行迭代操作,可能会被检测到结构变更,抛出ConcurrentModificationException

    如需解决上述问题,需要在遍历的时候给整个容器对象加锁,如startIteratorThread可以改为:

     

     

    6. 面试扩展

    1) JUC中的容器都是无锁的吗?

     

     

    第03章_异步执行

    第一节 线程池

    线程池实现资源共享的一种方式,主要由工作线程任务队列两个概念组成,它可以重用线程,减少线程创建的开销。

     

    1. 基本使用

    1) 创建和使用线程池

    在Java中,默认的线程池实现类为ThreadPoolExecutor,常用方法如下:

    使用示例如下:

    注意:

    1. ThreadPoolExecutor继承自AbstractExecutorService,顶层接口为Executor,了解即可。

     

    2) 获取线程池信息

     

    3) 配置线程池大小

    除了在创建时配置线程池大小外,还可以通过getter/setter方法获取和设置线程池大小。

    注意:

    1. 线程池的核心参数可通过hippo4j等框架进行监控和修改。

     

    4) 选择任务队列

    ThreadPoolExecutor要求的队列类型必须是阻塞队列BlockingQueue,可以是:

    注意:

    1. 如果使用无界队列,则线程个数最大只能达到corePoolSize,设置maximumPoolSize参数将无意义。

     

    5) 配置任务拒绝策略

    线程池一般使用有界队列maximumPoolSize是有限的,当两者都达到上限时,就会拒绝任务的提交(execute/submit/invokeAll),可以通过构造方法或setter方法设置拒绝策略

    ThreadPoolExecutor内部实现了四种拒绝策略可供选择:

    注意:

    1. 拒绝策略只有在队列有界,且最大线程数有限的情况下才会触发,让拒绝策略有机会执行对保证系统稳定非常重要。
    2. 在任务量非常大的场景中,如果队列无界,可能会导致请求处理队列积压过多任务,消费非常大的内存;如果队列有界但不限制最大线程数,可能会创建过多的线程,占满CPU和内存。
    3. 如需保证任务不被丢弃,则必须使用CallerRunsPolicy或进行任务持久化存储(自定义拒绝策略/实现混合阻塞队列)。

     

    6) 自定义线程工厂

    线程工厂可以对创建的线程进行一些配置,如设置线程名称等:

    默认实现类为Executors类中的静态内部类DefaultThreadFactory,它主要就是创建一个线程,给线程设置一个名称( pool-<线程池编号>-thread-<线程编号>),设置daemon属性为false,设置线程优先级为标准默认优先级等。

     

    7) 线程池的其他配置

     

    8) 线程池工厂类

    线程池工厂类Executors提供了一些静态工厂方法,可以方便的创建一些预配置的线程池,主要方法有:

    注意:

    1. 在系统负载可能极高的情况下,FixedThreadPool 的问题是队列过长,而 CachedThreadPool 的问题是线程过多。
    2. 一般情况下,建议自定义 ThreadPoolExecutor,使用有界队列,控制线程创建数量

     

     

    2. 源码分析

    1) 核心属性

     

    2) 状态流转

    线程池状态只能单向流转,即:RUNNING -> SHUTDOWN/STOP -> TIDYING -> TERMINATED。

    image-20251210171616822

    3) 执行流程

    image-20251210174448070

     

    4) 构造方法

    有参构造主要是对核心线程数、最大线程数,以及线程工厂、拒绝策略进行一些设置和校验,注意下核心线程数允许为0就OK。

     

    5) 执行任务(execute)

    execute方法是提交任务到线程池的核心方法,线程池的执行流程就是在指execute方法内部做了哪些判断:

    image-20251210172146538

    submit方法是在execute方法的基础上做的一层封装,执行的是可异步获取结果的FutureTask对象

     

    6) 工作线程(Worker#run)

    工作线程Worker循环获取任务并执行,执行时支持中断。

     

    7) 立即关闭(shutdownNow)

    执行shutdownNow方法,直接将线程池从RUNNING状态转变为STOP状态。

     

    8) 正常关闭(shutdown)

    执行shutdown方法,可以将线程池从RUNNING状态先转变为SHUTDOWN状态,该状态不会中断正在干活的线程,而且会处理阻塞队列中的任务。

     

    3. 面试扩展

    1) 线程池有哪些核心参数?

     

    2) 线程池处理任务的流程?

    图解线程池实现原理

     

    3) 线程空闲时在干嘛?

     

    4) 线程池线程抛出异常怎么处理?

     

    5) 线程池线程数设置多少合适?

     

    6) 线程池最佳实践?

     

    7) 线程池可能出现死锁吗?

    如果提交的任务之间存在依赖,在线程池打满时,可能会出现死锁。

    上述问题可以将线程池类型替换为newCachedThreadPool来解决,让创建的线程不再受限。

    另外,创建线程池时使用SynchronousQueue队列也可解决上述问题:

    对于普通队列,入队只是把任务放到了队列中,而对于SynchronousQueue来说,入队成功就意味着已有线程接受处理,如果入队失败,可以创建更多线程直到maximumPoolSize,如果达到了maximumPoolSize,会触发拒绝机制,不管怎么样,都不会死锁。

     

     

    第二节 定时任务

    在Java中,定时任务可以使用java.util包中的TimerTimerTask实现,也可以使用并发包中的ScheduledExecutorService实现。

    注意:

    1. 上述两者都不能胜任复杂的定时任务调度,如每周一和周三晚上18:00到22:00,每半小时执行一次。
    2. 对于类似这种需求,可以使用更为强大的第三方类库,比如Quartz(http://www.quartz-scheduler.org/)。

     

    1. Timer和TimerTask

    1) 基本使用

    TimerTask表示一个定时任务,它是一个抽象类,实现了Runnable接口,具体的定时任务需要继承该类,实现run方法。

    Timer表示一个定时器,负责定时任务的调度和执行,它有如下主要方法:

    注意:

    1. 对于固定延时的任务,下次执行时间为当前任务执行前的时间加上period,这并不符合一般的期望。
    2. 对于固定频率的任务,它总是基于最先的任务计划的,所以,很有可能会出现一下子执行很多次任务的情况。

     

    2) 实现原理

    Timer内部主要由Timer线程任务队列两部分组成

     

    3) 应用示例

     

    4) 注意事项

    一个Timer对象只有一个Timer线程,这意味着,定时任务不能耗时太长,更不能是无限循环,看个例子:

    Timer线程在执行任何一个任务的run方法时,一旦run抛出异常,Timer线程就会退出,从而所有定时任务都会被取消

    所以,如果希望各个定时任务不互相干扰,一定要在run方法内捕获所有异常

     

     

    2. ScheduledThreadPoolExecutor

    1) ScheduledThreadPoolExecutor简介

    由于Timer/TimerTask的一些问题,Java并发包引入了ScheduledExecutorService,它是一个接口:

    返回类型都是ScheduledFuture,它也是一个接口,实现了FutureDelayed,没有定义额外方法。

    注意:

    1. 与Timer不同,ScheduledExecutorService不支持以绝对时间作为首次运行的时间。

    主要实现类为ScheduledThreadPoolExecutor,它是线程池ThreadPoolExecutor的子类,是基于线程池实现的,构造方法如下:

    工厂类Executors也提供了一些方便的方法,以方便创建ScheduledThreadPoolExecutor,如下所示:

    注意:

    1. 它的任务队列是一个无界的优先级队列,所以最大线程数对它没有作用。
    2. 此外,即使corePoolSize设为0,它也会至少运行一个线程。

     

    2) 应用示例

    由于可以有多个线程执行定时任务,一般任务就不会被某个长时间运行的任务所延迟了,比如,对于前面的TimerFixedDelay,如果改为:

    再次执行,第二个任务就不会被第一个任务延迟了。

    另外,与Timer不同,单个定时任务的异常不会再导致整个定时任务被取消了,即使背后只有一个线程执行任务,我们看个例子:

    TaskA和TaskB都是每秒执行一次,TaskB两秒后执行,但一执行就抛出异常,屏幕的输出类似如下:

    这说明,定时任务TaskB被取消了,但TaskA不受影响,即使它们是由同一个线程执行的。不过,需要强调的是,与Timer不同,没有异常被抛出来,TaskB的异常没有在任何地方体现。所以,与Timer中的任务类似,应该捕获所有异常

    注意:

    1. 如果某个任务抛出了异常,那么该任务将不会被重新调度,即使它是一个重复任务。

     

    3) 核心属性

     

    4) 任务调度(schedule)

    schedule方法就是将任务和延迟时间封装到一起,并且将任务扔到阻塞队列中,再去创建工作线程去take阻塞队列。

     

    5) At和With方法&任务的run方法

    这两个方法在源码层面上的第一个区别,就是在计算周期时间时,需要将这个值传递给period,基于正负数在区别At和With

    所以查看一个方法就ok,查看At方法:

     

     

    3. 面试扩展

    1) 对比Timer/TimerTask

    ScheduledThreadPoolExecutor的实现思路与Timer基本是类似的,都有一个基于堆的优先级队列,保存待执行的定时任务,主要不同有:

     

     

     

    第三节 异步编程

    1. FutureTask

    1) FutureTask简介

    FutureTask 实现了 Future 和 Runnable 接口,表示可取消的异步任务,主要作用如下:

     

    2) 基本使用
    3) 核心属性&构造

    FutureTask 是基于 AQS 实现的,通过状态机管理任务生命周期,核心属性如下:

    4) run方法实现
    5) cancel方法实现

    任务取消根据参数是否允许中断有两种状态变化:

    6) get方法实现

     

     

    2. CompletableFuture-基本使用

    1) CompletableFuture简介

    CompletableFuture 是对 Future 接口的增强,用于对异步任务进行编排组合,是 Java 实现非阻塞异步编程的核心类。

    使用示例如下:

    注意:

    1. 可以通过executor参数指定执行线程池,如果省略,默认使用ForkJoinPool.commonPool()执行。
    2. Async后缀的函数表示该任务可能被提交到单独线程池中,从而相对前置任务来说是异步运行的。
    3. 同步处理函数(不带Async)被注册后,如果任务线程未结束,将会使用任务线程执行,如果已结束,将会由当前注册线程执行。

     

    2) 完成和取消任务

     

    3) 获取任务状态

     

    4) 获取任务结果

     

    5) 任务完成回调

    任务完成回调方法可以接收前一个任务正常结束时的结果值,或前面链路中的任务异常结束时的异常(抛出异常后,链路中的后续任务将不会继续执行),无返回值,不会改变原结果或覆盖原异常

    注意:

    1. 注册的异常处理逻辑对前面链路中的任务都生效,可参考下方“顺序任务流”案例。

     

    6)结果或异常处理

    结果或异常处理方法也可以接收正常结束时的结果值,或异常结束时的异常,但可以修改任务结果,且会覆盖原异常

     

     

     

    3. CompletableFuture-任务编排

    1) 依赖前一阶段正常完成

    下面一些方法可以用来构建依赖单一阶段的任务流,当前一个阶段正常完成时,自动触发所有依赖该阶段的下一阶段任务,如果前一个阶段发生了异常,所有后续阶段都不会执行,结果会被设为相同的异常,调用join会抛出运行时异常CompletionException

    简单示例如下:

    注意:

    1. 以run、accept、apply开头的方法,参数类型一般为Runnable、Consumer、Function类型。
    2. 在调用 thenXxx 时,如果前一阶段任务已经结束,那么将会在调用者线程执行子任务,可以使用 thenXxxAsync 避免该情形。

     

    2) 依赖前两阶段都正常完成

    当一个阶段正常完成,且指定的另一个阶段也正常完成时,才触发下一阶段。注意,这两个阶段可以并行执行,并且没有依赖关系。

     

    3) 依赖前两阶段之一正常完成

    当前阶段和指定的另一个阶段,只要其中一个正常完成,就会启动下一阶段任务。

     

    4) 构建依赖多个阶段的任务流

    如果依赖的阶段不止两个,可以使用如下静态方法,基于多个CompletableFuture构建了一个新的CompletableFuture。

     

     

    4. CompletableFuture-源码分析

    1) 核心属性

     

    2) 执行当前任务

    将任务和CompletableFuture封装到一起,再执行封装好的具体对象的run方法即可:

     

    3) 编排后续任务

    首先如果要在前继任务处理后,执行后置任务的话。

    有两种情况:

    如果单独采用thenRun在一个任务后面指定多个后继任务,CompletableFuture无法保证具体的执行顺序,而影响执行顺序的是前继任务的执行时间,以及后置任务编排的时机。

     

    4) 执行后续任务

    任务在编排到前继任务时,因为前继任务已经结束了,这边后置任务会主动的执行:

    前继任务执行完毕后,基于嵌套的方式执行后置。

     

    5) 流程图总结

    image-20251213230106441

     

    5. 面试扩展

    1) Runnable vs Future vs FutureTask

     

     

    第四节 Fork/Join框架

    1. Fork/Join框架简介

    Fork/Join框架是一个并行任务执行框架,它把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果,适用于多核环境的可分割计算再合并结果的计算密集型任务。

    img

    注意:

    1. ForkJoinPool不是为了替代ExecutorService,而是它的补充,在某些应用场景下(计算密集型任务)性能比ExecutorService更好。
    2. ForkJoinPool主要用于实现分而治之的算法,特别是分治之后递归调用的函数,例如QuickSort等;
    3. ForkJoinPool的宗旨是使用少量的线程来处理大量的任务,多个线程获取到多个处理器的时间分片,并行的执行子任务。

     

     

    2. Fork/Join框架原理

    Fork/Join框架其实就是指由ForkJoinPool作为线程池、ForkJoinTask作为异步任务、ForkJoinWorkerThread作为执行任务的线程这三者构成的任务调度机制

     

    1) ForkJoinPool

    ForkJoinPool是ExecutorService的一个实现,用于管理工作线程,以及提供获取线程池状态和性能信息的相关方法。

    ForkJoinPool中的每个工作线程都有自己的双端队列(WorkQueue)用于存储任务(ForkJoinTask),并且使用了一种名为工作窃取(work-stealing)算法来平衡线程的工作负载。

    注意:

    1. 工作窃取(work-stealing)算法指空闲的线程试图从繁忙线程的队列中(队首)窃取任务。

     

    2) ForkJoinWorkerThread

    ForkJoinWorkerThread直接继承了Thread,是被ForkJoinPool管理的工作线程,由它来执行ForkJoinTask。

     

    3) ForkJoinTask<V>

    ForkJoinTask抽象类表示一个可分割计算再合并结果的异步任务,常用方法如下:

    它有两个子抽象类RecursiveActionRecursiveTask,分别表示无返回结果和有返回结果的异步任务,我们一般使用它们。

     

     

    3. Fork/Join的使用

    注意:

    1. 如果拆分逻辑比计算逻辑还要复杂时,ForkJoinPool并不会带来性能的提升,反而可能会起到负面作用。