Java并发

Java并发总览

  • 原子性:一个或多个操作,要么全部执行完且中间不被任何干扰,要么一个都不执行。
  • 可见性:一个线程对共享变量的修改,其他线程能立刻看到。
  • 有序性:程序执行的顺序,按照代码的书写顺序来。

Java内存模型

[!info] JMM 主要解决可见性、有序性,基本保证原子性操作。

JMM总览

JMM是什么

Java内存模型(Java Memory Model),简称JMM,是一套抽象的规范。定义了多线程环境中共享变量的访问规则。 JMM将内存划分为主内存和工作内存。

  • 主内存:所有线程共享,存放变量的正式值。
  • 工作内存:每个线程私有,存放该线程用到的主内存变量的副本

JMM内存模型

JMM解决什么问题

JMM解决可见性、有序性两个问题:

  • 可见性问题:CPU多级缓存与缓冲区。
  • 有序性问题:编译器和处理器为了性能会进行指令重排序。

解决可见性

建立“强制刷新/失效”协议。JMM规定,线程对变量的操作不能一直停留在工作内存里,必须在特定时刻同步到主内存。

  • volatile:
    • 写操作:新值必须立即刷新到主内存。
    • 读操作:每次读取前强制从主内存重新加载,并让其他线程的副本实效。
  • synchronized:
    • 加锁后:必须清空工作内存中变量副本,强制从主内存重新加载。
    • 解锁前:工作内存的修改必须全部刷新到主内存。
  • final:
    • 只要构造期间没有让this引用逸出,构造完成后final字段的值对其他线程立刻可见,无需额外同步。

解决有序性

定义Happens-Before原则。规定哪些操作不可重排序。

保证基本的原子性

JMM只保证基本读写操作的原子性,除了long、double外,其他变量的单次读写操作都是原子的。复合操作必须通过锁或其他方式保证原子性。

JMM怎么实现

JMM的规范是抽象的, 需要靠JIT编译器插入内存屏障处理器提供的硬件指令来落地。

内存屏障

JIT在编译字节码时,会在关键位置插入四种内存屏障指令。

屏障类型 作用
LoadLoad 禁止屏障前后的读操作重排
StoreStore 禁止屏障前后的写操作重排
LoadStore 禁止屏障前的读与屏障后的写重排
StoreLoad 禁止屏障前的写与屏障后的读重排(最重,同时具备其他三者效果)
  • volatile:
    • 写操作:写前插入 StoreStore,写后插入 StoreLoad。
    • 读操作:读后插入 LoadLoad 和 LoadStore。
  • synchronized:使用字节码指令monitorentermonitorexit触发内存屏障,保证临界区内的读写在锁释放后可见。
  • final:在构造方法末尾插入 StoreStore屏障,保证final字段赋值不会与对象引用赋值被重排序。

硬件指令

内存屏障主要通过CPU指令lock xxx实现。

  • 强制将当前CPU缓存刷新到主内存。
  • 通过MESI缓存一致性协议,将其他CPU缓存中对应数据失效。
  • 阻止处理器对lock前后的指令进行重排序。

volatile

[!NOTE] volatile 最轻量的同步机制,通过写前StoreStore+写后StoreLoad、读后LoadLoad+LoadStore的内存屏障策略,保证多线程环境下的可见性和有序性,但不保证原子性。

  • 可见性:
    • 写入:volatile写后插入 StoreLoad屏障,强制将当前线程工作内存刷新到主内存。
    • 读取:强制从主内存加载。
  • 有序性:
    • 写入:写前插入 StoreStore,写后插入StoreLoad。
    • 读取:读后插入 LoadLoad、LoadStore。

synchronized

[!NOTE] synchronized 较重的同步机制,保证原子性、可见性、有序性。

synchronized用法:

  • 修饰普通方法:锁当前实例对象this。
  • 修饰静态方法:锁当前类对象。
  • 修饰代码块:锁指定的对象。

如何保障的原子性、可见性、有序性:

  • 原子性:锁互斥。同一时刻只有一个线程能进入临界区执行。
  • 可见性:加锁后,工作内存被清空,强制从主内存重新加载;解锁前,强制刷新到主内存。
  • 有序性:保证临界区内的代码不会与临界区外的代码发生重排序(即代码不能跨出或跨入临界区),但临界区内部的指令是允许重排序的。

底层实现原理:

  • 字节码层:临界区代码由monitorentermonitorexit包裹,方法修饰符中设置了 ACC_SYNCHRONIZED 标志。确保同一时刻,只有一个线程能获取对象关联的Monitor。
  • 对象头与Monitor:每个对象的对象头都有一个Mark Word,记录了锁状态(无锁、偏向锁、轻量级锁、重量级锁)。重量级锁状态下,Mark Word指向一个ObjectMonitor。
  • 硬件层:原子性由CAS 或 操作系统互斥量保障。可见性和有序性由内存屏障保障。

锁升级:

  • 无锁:没有加锁的情况下。
  • 偏向锁:消除同一线程反复获取锁的同步开销。线程第一次获取到锁时,通过CAS在对象头的Mark Word记录下自己的线程ID。之后进入同步块,只需判断线程ID是否一致来确定是否是当前线程持有锁。
  • 轻量级锁:并发情况下,通过CAS自旋来获取锁,获取成功后将对象头的Mark Word修改为指向当前线程在栈中创建的Lock Record。
  • 重量级锁:自旋失败或竞争激烈时,膨胀为重量级锁。JVM创建ObjectMonitor,未获取到锁的线程进入等待队列,被操作系统挂起,直到锁释放后被唤醒。

final

[!info] final 解决不可变对象的安全发布问题。

核心保证是:当一个对象的构造函数执行完毕,且 this 引用没有在构造期间逸出,那么其他线程即使没有同步,也能立刻看到该对象中所有 final 字段的正确初始化值。

实现原理:在构造函数执行完毕、即将返回时,JIT 编译器插入了 StoreStore 内存屏障。确保在构造方法返回前,final变量已安全发布。

PS:只有包含final字段的构造方法,才会插入StoreStore屏障。

AQS

AQS,全称AbstractQueuedSynchronizer,是一个抽象的、基于FIFO等待队列的、用一个volatile ine表示同步状态的框架。

AQS解决的问题是:把线程如何安全地排队、阻塞、唤醒等这些复杂且容易出错的操作封装起来,让开发者只需关心何时加锁、何时释放的上层逻辑。

AQS使用模板方法模式定义:

  • 线程如何安全地入队、出队。
  • 如何用LockSupport.park/unpark挂起和唤醒线程。
  • 如何处理中断和超时。

AQS数据结构:

  • 状态变量state:表示同步状态,可以是重入锁或许可证。
  • FIFO等待队列:一个由Node节点构成的双向链表,头节点表示当前持有锁或正在获取锁的线程。
  • 等待状态waitStatus:每个Node节点有一个waitStatus。
    • SIGNAL(-1):表示后继节点需要被唤醒。当前节点释放锁时,必须检查并unpark后继节点。
    • CANCELLED(1):节点因超时或中断被取消。

AQS的两种获取锁模式:

  • 独占模式:
    • 核心特征:同一时刻只有一个线程能成功获取资源。
    • 典型实现:ReentrantLock。
    • 状态变量state含义:锁支持次数(0=空闲,1=持有,>1=重入)。

AQS独占模式

  • 共享模式:
    • 核心特征:同一时刻多个线程可以同时成功获取资源。
    • 典型实现:Semaphore、CountDownLatch。
    • 状态变量state含义:剩余许可证/资源数量。

AQS共享模式

ReentrantLock、Semaphore、CountDownLatch等都是基于AQS实现的。

ReentrantLock

[!NOTE] 锁模式 独占模式

ReentrantLock是一个可重入的互斥锁,内部有一个继承自AQS的Sync,并分为公平锁非公平锁两种实现,默认为实现为非公平锁

加锁逻辑:

  • lock():调用acquire(1)
  • tryAcquire(1):
    • 若state等于0,则CAS获取锁,成功则设为锁持有者。
    • 若当前线程已是持有者,则state+1,表示重入。
    • 若CAS失败,则进入等待队列。

解锁逻辑:

  • unlock():调用release(1)
  • tryRelease(1):state-1,若state清零,则释放锁,并唤醒后继节点。

两种锁实现:

  • 公平锁:先检查队列里有没有线程在等,有就乖乖排队。
  • 非公平锁:一上来就CAS抢锁,不看队列里有没有线程排队。

Semaphore

[!NOTE] 锁模式 共享模式

Semaphore是信号量,将state表示为剩余许可证数量,来控制多个线程访问资源。内部有一个继承自AQS的Sync,并分为公平锁非公平锁两种实现,默认为实现为非公平锁

加锁逻辑:

  • acquire():调用acquireSharedInterruptibly(1)
  • tryAcquireShared(1):
    • 若state大于0,CAS更新state-1,表示当前线程获取一个许可。
    • 若state小于0,返回负数,进入等待队列。

解锁逻辑:

  • release():调用releaseShared(1)
  • tryReleaseShared(1):CAS更新state+1,表示归还一个许可。

若剩余许可证数量大于0,AQS会自动唤醒下一个等待的线程,直到许可证为0。

CountDownLatch

[!NOTE] 锁模式 共享模式

CountDownLatch是倒计时门闩,将state表示为还需等待的事件数量,是一个一次性的共享模式实现

加锁逻辑:

  • await():调用acquireSharedInterruptibly(1)
  • tryAcquireShared(1):
    • 若state等于0,直接放行。
    • 若state大于0,则进入等待队列。

解锁逻辑:

  • countDown():调用releaseShared(1)
  • tryReleaseShared(1):CAS更新state-1,当state等于0,释放锁成功,并触发唤醒。

Semaphore和CountDownLatch的关键区别:

  • Semaphore:state可复用。
  • CountDownLatch:state是一次性的,当state等于0,所有等待线程被唤醒,门闩永久打开。

AQS实现比对

特性 ReentrantLock Semaphore CountDownLatch
模式 独占 共享 共享
state 含义 锁持有次数 剩余许可证数 还需等待的事件数
获取 lock() → state 0→1 acquire() → state 减 N await() → 等 state==0
释放 unlock() → state 减 1 release() → state 加 1 countDown() → state 减 1
可重入
锁支持 公平锁/非公平锁 公平锁/非公平锁 ❌无此概念
复用性 可复用 可复用 一次性
唤醒传播 无(独占) 有(共享) 有(共享,到 0 时)

ThreadLocal

ThreadLocal是一个线程级别的隔离工具。每个线程有一份独立的存储,可以避免线程间的数据共享和竞争。

核心原理:每个Thread对象有一个ThreadLocalMap。ThreadLocalMap是一个哈希表,key为ThreadLocal对象(弱引用),value为需要存储的值。

key为什么设计为弱引用? 为了在ThreadLocal对象失去外部引用后,key可以被GC回收。这样ThreadLocalMap里的key为null,但value还被强引用。所以必须通过remove来清理,防止内存泄漏。

线程池

线程池是一种池化资源技术,它预先创建一定数量的线程,放入“池子”中。当有任务需要执行时,从池子里取一个线程来执行,任务执行完成后线程不会销毁,而是返回池子等待下一个任务。

为什么需要线程池?

  • 降低资源消耗:避免频繁创建、销毁线程的开销。
  • 提高响应速度:任务到达时,无需等待线程创建就可以立即执行。
  • 提高线程的可管理性:可以统一分配、调优和监控线程的数量和状态。

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

ThreadPoolExecutor是线程池的核心实现类,提供了以下参数:

  • corePoolSize:核心线程数,即使空闲也不会回收。
  • maximumPoolSize:线程池允许的最大线程数。
  • keepAliveTime:当线程池大于核心线程数时,多余空闲线程的存活时间。
  • unit:存活时间的单位。
  • workQueue:线程等待队列。
  • threadFactory:线程工厂,可以自定义线程名称。
  • handler:拒绝策略,当队列满且达到最大线程数时,拒绝线程的策略。

线程池执行流程

  • 当前线程数 < 核心线程数,则创建核心线程执行。
  • 当前线程数 = 核心线程数,且队列未满,则将线程放入等待队列。
  • 队列已满,但当前线程数 < 最大线程数,则创建非核心线程执行。
  • 队列已满,且当前线程数 >= 最大线程数,则执行拒绝策略。

核心线程数是常驻线程,最大线程数是临时的救急线程。

Executors

Executors是JUC包中的一个线程池工厂类,用于快速创建ExecutorService实例。

Executors默认提供了几个工厂方法:

  • newFixedThreadPool:
    • 线程池介绍:固定大小的线程池。
    • 核心线程数:指定大小。
    • 最大线程数:指定大小。
    • 等待时间:0
    • 等待队列:无界队列LinkedBlockingQueue。
  • newCachedThreadPool
    • 线程池介绍:缓存线程池。
    • 核心线程数:0。
    • 最大线程数:无限大。
    • 等待时间:60s。
    • 等待队列:无容量队列SynchronousQueue。
  • newSingleThreadExecutor:
    • 线程池介绍:单个线程的线程池。
    • 核心线程数:1。
    • 最大线程数:1。
    • 等待时间:0。
    • 等待队列:无界队列LinkedBlockingQueue。
  • newScheduledThreadPool:
    • 线程池介绍:定时调度线程池。
    • 核心线程数:指定大小。
    • 最大线程数:无限大。
    • 等待时间:10s。
    • 等待队列:延迟队列DelayedWorkQueue。

《阿里巴巴 Java 开发手册》规定:不允许使用 Executors 创建线程池,必须通过 ThreadPoolExecutor 手动创建。 使用默认线程池工厂方法的风险:

  • OOM:等待队列无限大,可能发生OOM。
  • 资源耗尽:最大线程数无限大,可能导致资源耗尽。

等待队列

线程池使用的队列全部来自 java.util.concurrent.BlockingQueue接口的实现:

  • ArrayBlockingQueue:数组阻塞队列,有界,必须指定容量。
  • LinkedBlockingQueue:线性阻塞队列,默认无界,可指定有界。
  • SynchronousQueue:同步阻塞队列,无容量,直接提交。
  • PriorityBlockingQueue:优先级阻塞队列,无界,按优先级提交。
  • DelayedWorkQueue:延迟阻塞队列,无界,延迟提交。

ArrayBlockingQueue和LinkedBlockingQueue的区别:

  • 内存占用控制:ArrayBlockingQueue的内存占用更可控一些,可以指定大小,避免任务堆积。
  • 吞吐量:ArrayBlockingQueue使用单锁,LinkedBlockingQueue:双锁,吞吐量高。
  • 灵活性:对任务量变化较大的应用场景,LinkedBlockingQueue更灵活。

拒绝策略

当队列已满且达到最大线程数时,触发拒绝策略。

拒绝策略:

  • AbortPolicy:默认拒绝策略,直接抛出RejectedExecutionException异常,需要处理异常。
  • CallerRunsPolicy:由调用者执行任务。
  • DiscardPolicy:直接丢弃新任务,不会抛出异常。
  • DiscardOldestPolicy:丢弃队列中最老的任务。

线程池配置最佳实践

  • CPU密集型任务:以计算为主,线程数 = CPU核心数 + 1。
  • IO密集型任务:大量阻塞等待,线程数 = CPU核心数 * 2,或更精准的计算方式:CPU核心数 * (1+平均等待时间/平均计算时间)。

线程生命周期

  • NEW:线程刚创建,尚未启动。
  • RUNNABLE:可运行状态(包含等待调度和已抢占到时间片两种情况)。
  • BLOCKED:获取锁被阻塞,仅限synchronized锁。
  • WAITING:无限期等待直到被唤醒。常见如:ReentrantLock使用LockSupport.park()来阻塞线程、wait()、join()。
  • TIMED_WAITING:限时等待,超时自动返回。
  • TERMINATED:线程执行完毕或异常退出。

线程生命周期

原子类

FAQ

run方法和start方法的区别?


知识笔记 · 结构化总结 + 交互式知识图谱

Java 并发

从内存模型到锁、并发工具与线程池,一篇笔记的全景梳理。点击任意节点查看详情,可拖拽节点、滚轮缩放。

结构化总结

并发三大特性

原子性 · 可见性 · 有序性
  • 原子性:一组操作要么全做完且中途不被打断,要么都不做。
  • 可见性:一个线程对共享变量的修改,其他线程能立刻看到。
  • 有序性:程序按代码书写顺序执行(编译器/处理器会重排序破坏它)。

Java 内存模型 (JMM)

抽象规范 · 主内存 / 工作内存
  • 定义多线程下共享变量的访问规则,分主内存(共享,正式值)与工作内存(线程私有,变量副本)。
  • 解决可见性:强制刷新/失效协议(volatile、synchronized、final)。
  • 解决有序性:定义 Happens-Before 原则,规定哪些操作不可重排。
  • 基本原子性:除 long/double 外的单次读写原子;复合操作需加锁。
  • 落地方式:JIT 插入内存屏障(LoadLoad / StoreStore / LoadStore / StoreLoad)+ 硬件 lock 指令(MESI 缓存一致性)。

三个关键字

volatile · synchronized · final
  • volatile:最轻量同步。保证可见性 + 有序性,不保证原子性。写前 StoreStore / 写后 StoreLoad,读后 LoadLoad+LoadStore。
  • synchronized:较重,三性全保证。靠 monitorenter/exit + 对象头 Mark Word + ObjectMonitor。锁升级:无锁→偏向锁→轻量级锁→重量级锁。
  • final:解决不可变对象的安全发布。构造期 this 不逸出时,构造末尾插入 StoreStore 屏障保证 final 字段对其他线程可见。

AQS 与并发工具

AbstractQueuedSynchronizer
  • 基于 FIFO 双向队列 + volatile state 的框架,用模板方法封装排队/阻塞/唤醒(LockSupport.park/unpark)。
  • 独占模式:同一时刻仅一个线程,典型 ReentrantLock。
  • 共享模式:多线程同时获取,典型 Semaphore、CountDownLatch。
  • Node 的 waitStatus:SIGNAL(-1) 需唤醒后继;CANCELLED(1) 已取消。

ThreadLocal

线程级隔离
  • 每个 Thread 持有一个 ThreadLocalMap,key 为 ThreadLocal(弱引用),value 为存储值。
  • key 用弱引用,便于 ThreadLocal 失去外部引用后被 GC 回收。
  • 但 value 仍被强引用 → 必须 remove() 清理,防止内存泄漏

线程池

ThreadPoolExecutor · 池化技术
  • 为何用:降低创建/销毁开销、提高响应速度、便于管理监控。
  • 7 参数:核心数、最大数、keepAlive、单位、等待队列、线程工厂、拒绝策略。
  • 执行流程:核心线程 → 队列 → 非核心线程 → 拒绝策略。
  • 拒绝策略:Abort(默认抛异常)、CallerRuns、Discard、DiscardOldest。
  • 最佳实践:阿里规约禁用 Executors(OOM/资源耗尽风险);CPU 密集 = 核心数+1,IO 密集 ≈ 核心数×2。

线程生命周期

6 种状态
  • NEW 新建 → RUNNABLE 可运行(含就绪+运行)。
  • BLOCKED 等待 synchronized 锁。
  • WAITING 无限等待(park/wait/join);TIMED_WAITING 限时等待。
  • TERMINATED 执行完毕或异常退出。

AQS 三实现对比

ReentrantLock · Semaphore · CountDownLatch
特性ReentrantSemaphoreCountDown
模式独占共享共享
state持有次数剩余许可待等事件
可重入
复用可复用可复用一次性

源自笔记《Java 并发 · kit-docs》。图谱与卡片为结构化重组,便于复习与检索。