1. 基础概念

并发与并行

并发是指多个任务在同一时间段内被调度执行,宏观上“同时”,微观上可能交替执行,单核CPU的并发靠的是操作系统快速切换上下文。

并行是指多个任务真正同时执行,靠的是多核CPU。

Java的线程池是并发还是并行取决于CPU,单核CPU为并发,多核CPU为并行。

进程、线程、协程 概念与区别

特性

进程

线程

协程

调度

OS

OS

程序自身

内存隔离

独立

共享进程资源

共享线程资源

上下文切换开销

最大

中等

最小

并行能力

取决于线程数

典型数量

少(几十)

中(几百~几千)

多(几十万)

常见语言支持

OS 原生

Java、C++

Go、Kotlin、Python、Rust

进程是操作系统资源分配的基本单位,每个进程都有独立的内存空间,文件句柄,系统资源,进程之间相互隔离、安全性高、但切换开销大。

线程是CPU调度的基本单位,一个进程中可以包含多个线程,线程共享同一个进程的内存和资源,线程比进程轻量,但数据共享带来线程安全问题。

协程是比线程更轻量的“用户态线程”,不由操作系统调度,而由用户程序调度,切换不需要陷入内核态(比线程上下文切换更便宜),Java 本身没有原生协程,但 Kotlin 有(挂起函数),Java Loom 项目未来会支持虚拟线程(类似协程)。

协程本质可以理解为用户态的可挂起/恢复的函数,或者说可以暂停的函数。

线程上下文切换

当多个线程竞争 CPU 时,操作系统需要不断将线程暂停、恢复,这个过程就是上下文切换。

需要切换的内容有:程序计数器(PC)、线程私有栈、寄存器内容、内核态/用户态切换

由于频繁切换会影响性能,并发不是越多线程越好,过多反而降低性能

线程的生命周期

Java 线程有 6 种状态(Thread.State 枚举):

NEW:创建了线程对象,但还没 start()

RUNNABLE:可能正在运行,也可能正在等待 CPU 调度(Java 将就绪和运行合并)

BLOCKED:等待获取 synchronized 锁

WAITING:等待 notify()、join() 等(无限等待)

TIMED_WAITING:带超时的等待(如 sleep(1000))

TERMINATED:执行完 run() 方法

其中 RUNNABLE ≠ RUNNING,在Java 中 RUNNABLE 包括了 “就绪 + 运行” 两种状态,因为JVM 只关心这个线程“能不能运行”,只要线程不处于等待、定时等待、阻塞,只要它具备运行条件,就归为 RUNNABLE。

注意,所谓Java的线程状态是JVM规定的,而不是操作系统规定的。不同的操作系统对线程状态的划分不一样,如果直接照搬某一种划分,就无法实现跨平台性。

用户进程、守护进程

用户进程是程序的核心逻辑线程,例如 main、业务线程,JVM 必须等待用户线程全部结束才退出。

守护进程是为用户线程提供服务的线程,如 GC,当只剩守护线程时,JVM 直接退出

例如:

public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            try {
                while (true) {
                    System.out.println("daemon working...");
                }
            } finally {
                System.out.println("daemon finally");
            }
        });

        t.setDaemon(true);
        t.start();

        // 主线程(用户线程)很快结束
        System.out.println("main exit");
    }
}

代码输出为:

main exit
daemon working...
daemon working...
daemon working...
daemon working...
(JVM退出)

发现t线程中的finally代码没有执行,守护线程不是 JVM 正常关闭的阻碍,JVM 不会等待守护线程完成,因此守护线程的 finally 代码不一定能执行。

线程池中默认使用用户线程。

2. Java内存模型(JMM)

为什么需要JMM

JMM 是 Java 为解决并发语义问题,屏蔽不同 CPU、不同操作系统的内存行为而制定的抽象规范。

其目标是解决多线程读写共享变量时的三大问题:

可见性(线程看到的数据不一致)

原子性(操作被 CPU 断开)

有序性(指令重排)

内存结构:主内存 & 工作内存

JMM规定内存分为两部分:

主内存:所有共享变量存放的地方(Java堆/方法区),所有线程都能访问

工作内存:每个线程私有,相当于线程的CPU缓存副本

线程不会直接操作主内存,而是读写自己的工作内存

JMM解决的三大问题

  1. 原子性:

一个操作或多个操作要么全部执行且执行过程不被中断,要么全部不执行。例如i++看似简单,实则包含读取、修改、写入三个步骤,多线程环境下可能出现部分执行的情况。JMM 中,synchronized 和 JUC(java.util.concurrent)中的原子类(如 AtomicInteger)通过锁机制或CAS 操作保证原子性。

  1. 可见性:

当一个线程修改了共享变量的值,其他线程能否立即看到这个修改。在多核 CPU 架构中,每个线程可能拥有独立的缓存,若未遵循缓存一致性协议,就会导致 “线程 A 修改了变量,线程 B 却读取到旧值” 的现象。JMM 通过volatile 关键字、synchronized和final等机制,强制刷新缓存,保证变量修改的即时可见。

  1. 有序性:

程序执行的顺序是否与代码顺序一致。编译器的指令重排序、CPU 的乱序执行等优化可能改变代码实际执行顺序,在单线程下这是透明的优化,但多线程中可能导致逻辑错误(如 DCL 单例模式中的指令重排问题)。JMM 通过volatile、synchronized和happens-before 规则限制重排序,确保有序性。

Happens-before

是JMM中最重要的概念,所有并发行为都基于他。前一个操作的结果必须对后一个操作可见,并且前一个操作按顺序发生。

几个happens-before规则:

程序顺序规则(Program Order Rule):同一个线程内的代码按照程序顺序执行

监视器锁规则(synchronized):解锁(unlock) happens-before 之后的加锁(lock),解决可见性 + 原子性 + 有序性

volatile 规则:写 volatile happens-before 读 volatile,保证可见性 + 有序性(禁止重排)不保证原子性

线程启动规则(Thread Start Rule):thread.start() happens-before 线程内部的任何操作

线程终止规则(Thread Join Rule):线程结束 happens-before join() 返回

传递性:A happens-before B,B happens-before C → A happens-before C

volatile的内存语义

volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰,这就指示编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序。

为了优化性能,代码可能被 CPU 和编译器重排,不会改变单线程的最终执行结果,但多线程下重排会导致严重问题

Java 使用内存屏障指令(memory barrier)保证:读写行为不被重排+缓存一致性

典型屏障有LoadLoad Barrier、StoreStore Barrier、LoadStore Barrier、StoreLoad Barrier(最昂贵)

在 Java 内存模型中,volatile 写操作前后会插入 StoreStore + StoreLoad 屏障,volatile 读操作前会插入 LoadLoad + LoadStore 屏障,从而保证可见性和禁止重排序。

synchronized 会使用 monitorenter/monitorexit,间接插屏障

volatile规则可以保证可见性与有序性,但不保证原子性。

3. 线程的创建与管理

三种创建方式

Java 创建线程有三种最常见的方式:

继承 Thread 类(但Java只能单继承,继承 Thread 会限制继承其他类,因此实际开发几乎不用这种方式):

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running");
    }
}

new MyThread().start();

实现 Runnable 接口(推荐方式,可被多个线程共享,可继承):

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Running");
    }
}

new Thread(new MyTask()).start();

实现 Callable(带返回值的线程任务,能抛出异常,常用于线程池):

Callable<Integer> task = () -> {
    return 100;
};

FutureTask<Integer> ft = new FutureTask<>(task);
new Thread(ft).start();

Integer result = ft.get();

start() or run()

创建新线程需要调用start()方法而不是run()方法,只有调用start()方法 JVM 才会创建新的线程,而调用run()方法只是普通的方法调用,在当前线程执行。

线程的核心管理方法

sleep()

Thread.sleep(1000);

该方法不释放锁,不依赖monitor,会让出CPU,但保持线程状态为 TIMED_WAITING

yield()

Thread.yield();

让线程“尝试”让出 CPU,但不保证真的让出(操作系统可能无视)

join()(线程等待):

t.join();

当前线程等待另一个线程结束,本质通过 wait() + notifyAll() 实现

interrupt()(中断机制):

t.interrupt();

Java 的线程不能被强制中断,只能“请求中断”,线程内部需要配合:

if(Thread.currentThread().isInterrupted()) { ... }

线程的优先级

线程可以设置优先级:

thread.setPriority(Thread.MAX_PRIORITY);

但优先级不可靠,通常不依赖,而且不同 OS 对优先级支持差异很大,不保证生效。

线程的栈空间

可以设置线程的栈大小:

new Thread(null, runnable, "threadName", 2 * 1024 * 1024)

栈太大会增加内存消耗;栈太小可能导致 StackOverflowError

为什么不推荐手动创建线程

手动创建线程创建销毁开销大,无法管理线程数,无法复用线程,无法执行任务队列,缺乏拒绝策略,无法监控线程状态,实际开发中一般使用线程池。

4. ThreadLocal

什么是 ThreadLocal

ThreadLocal 并不是用来解决线程安全问题的,而是用来解决线程隔离问题。

ThreadLocal 的作用是:

ThreadLocal 是一种以 Thread 为宿主的数据隔离机制,通过 Thread 对象内部的 ThreadLocalMap,为每个线程维护一份独立的变量副本,从而避免线程间共享与同步问题。

每个 Java 线程都有一个对应的 Thread 对象,ThreadLocal 通过 Thread 对象内部的 ThreadLocalMap,为当前线程维护一份 ThreadLocal → value 的映射,Thread对象、ThreadLocal、value都存储在堆上。其本质目的就是提供线程基本的数据隔离。

常见应用场景:

  • 保存用户上下文(userId、traceId)

  • 保存数据库连接 / Session

  • 保存事务上下文

  • 链路追踪(MDC)

ThreadLocal 的基本用法

ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("hello");
String value = tl.get();
tl.remove();

每个线程调用 set() / get() 访问到的,都是当前线程自己的变量副本。

ThreadLocal 的底层原理

核心结构

ThreadLocal 的数据并不存储在 ThreadLocal 对象中,而是存储在线程内部:

class Thread {
    ThreadLocal.ThreadLocalMap threadLocals;
}

真正的数据结构是 ThreadLocalMap,属于 Thread 对象。

ThreadLocalMap 的结构

ThreadLocalMap 是 ThreadLocal 的静态内部类,本质是一个定制化的 HashMap:

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
    }
}

关键点:

  • key:ThreadLocal(弱引用)

  • value:实际存储的值(强引用)

引用类型

是否会被 GC

强引用

普通引用

永不

软引用

SoftReference

内存不足时

弱引用

WeakReference

下一次 GC

虚引用

PhantomReference

随时

为什么 ThreadLocal 的 key 是弱引用?

设计原因

如果 key 使用强引用:

Thread → ThreadLocalMap → ThreadLocal(key)

即使外部 ThreadLocal 变量已经不可达,也无法被 GC,导致内存泄漏。

使用 WeakReference

  • 当 ThreadLocal 对象没有外部强引用时

  • key 会被 GC 回收

那为什么value是强引用?

JVM无法预判这个 value 后面还要不要用,为了保证线程上下文数据在使用期间的稳定性和可预测性,需要由程序员显式 remove 来管理生命周期

ThreadLocal 为什么仍然会内存泄漏?

弱引用 key 被回收,但 value 是强引用

泄漏场景:

Thread (线程池中的线程,长期存活)
 └── ThreadLocalMap
      └── Entry (key=null, value=Object)
  • key 被 GC 回收 → 变成 null

  • value 仍然强引用

  • 线程不结束 → value 永远无法释放

什么时候 ThreadLocal 最容易泄漏?

线程池 + ThreadLocal:

ExecutorService pool = Executors.newFixedThreadPool(10);

ThreadLocal<User> tl = new ThreadLocal<>();

pool.submit(() -> {
    tl.set(user);
    // 忘记 remove()
});
  • 线程池线程长期复用

  • ThreadLocalMap 不会销毁

  • value 持续堆积

如何正确使用 ThreadLocal

必须在 finally 中 remove:

try {
    tl.set(value);
    // 业务逻辑
} finally {
    tl.remove();
}

这是使用 ThreadLocal 的铁律。

InheritableThreadLocal

InheritableThreadLocal 可以让子线程继承父线程的 ThreadLocal 值:

InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();

注意:

  • 只在线程创建时拷贝

  • 线程池中的线程是复用的 → 不生效

TransmittableThreadLocal(TTL)

阿里开源的 TransmittableThreadLocal

  • 解决线程池中上下文无法传递的问题

  • 在任务提交时复制上下文

  • 常用于日志 traceId、链路追踪

ThreadLocal 与并发安全的关系

ThreadLocal ≠ 线程安全
ThreadLocal = 线程隔离

ThreadLocal 通过不共享数据避免并发问题,而不是通过加锁。

ThreadLocal 使用总结

维度

说明

解决问题

线程隔离

是否加锁

不需要

底层

ThreadLocalMap

key

WeakReference

value

强引用

最大风险

内存泄漏

必须操作

remove()

高危场景

线程池

5. synchronized

原理

synchronized 是 Java 中最重要的同步手段,保证原子性、有序性、可见性。

其底层实现为对象头:Java 的每一个对象都有对象头(Object Header),其中一部分叫 Mark Word,存储着synchronized 的锁信息,结构如下:

内容

描述

锁标志位

表示现在对象处于哪种锁状态

hashCode

对象哈希

GC 分代年龄

GC 使用

线程 ID

用于记录锁归属

不同锁状态下,Mark Word 的内容也不同。

而在编译后,源代码:

synchronized(obj) {
}

会变为:

monitorenter
...
monitorexit

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。对象锁的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。

线程申请 monitor 时,若发现已经是自己持有,就会直接进入,所以说synchronized 是可重入锁。

synchronized 内存语义

synchronized 的 happens-before 语义:

  1. 进入 synchronized(monitorenter)之前 → 会清空当前线程的工作内存

  2. 退出 synchronized(monitorexit)之后 → 会将工作内存刷新到主内存

  3. 锁的释放 happens-before 锁的获取

三种使用方式

修饰实例方法(锁对象:当前实例 this):

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

等价于:

public void increment() {
    synchronized (this) {
        count++;
    }
}

该方式锁的是 当前对象实例,不同实例之间 互不影响,是最常见、最安全的写法之一

适用于对象级别的共享数据,单例 / Spring Bean 中的实例变量

修饰静态方法(锁对象:Class 对象):

public class Counter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }
}

等价于:

public static void increment() {
    synchronized (Counter.class) {
        count++;
    }
}

该方式锁的是 Class 对象,全 JVM 中该类只有一把锁,比实例锁粒度更大

适用于静态变量,全局共享资源

synchronized 代码块(锁对象:任意对象):

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
}

锁对象可以自定义,锁粒度最小、性能最好,是最推荐的写法(控制更精细)

重量级锁?

在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

不过,在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。因此, synchronized 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized

锁升级机制

锁有四个状态,按低到高依次是:

  1. 无锁

  2. 偏向锁(Biased Lock)

  3. 轻量级锁(Lightweight Lock)

  4. 重量级锁(Heavyweight Lock)

锁只能升级,不能降级(除了自旋锁内部优化)

无锁→偏向锁:

第一次加锁时,将对象头 Mark Word 设置为:

偏向模式

持有偏向的线程 ID

下次同一线程再进入锁 → 直接进入,不做任何同步操作

适用于锁长期由同一线程持有,如 StringBuffer、单线程执行同步方法

偏向锁→轻量级锁:

当第二个线程竞争锁时,偏向锁撤销,升级为轻量级锁。

线程会在栈中创建一个 Lock Record(锁记录),尝试用 CAS 将对象头 Mark Word 替换为指向 Lock Record 的地址,如果 CAS 成功则获得锁,失败则表明有竞争,自旋。

轻量级锁比重量级锁快,但自旋会占用 CPU

轻量级锁→重量级锁:

当自旋失败次数达到阈值时,JVM 将锁升级为 “重量级锁”,进入阻塞(BLOCKED),队列管理等待线程,解锁时唤醒下一个线程

重量级锁本质上依赖 OS 的互斥量(mutex),涉及内核态切换,开销最大

6. 锁与并发工具类

LockSupport

是 Java 并发阻塞/唤醒的最底层操作,AQS 内部所有阻塞机制都基于 LockSupport 实现。

提供两个原语:

park();
unpark(thread);

特点:

  • 不需要获得锁

  • unpark 先发生不会丢失(与 notify 不同)

  • 用 Unsafe.park/unpark 实现

  • 允许构建各种同步器(AQS 基于它)

AQS(AbstractQueuedSynchronizer)

AQS用一个状态值(state)和一个 FIFO 队列(CLH 队列)来实现线程阻塞/唤醒,几乎所有锁都基于 AQS。

AQS 的核心组成:

一个 volatile int state(同步状态)

一个 FIFO CLH 双向队列

acquire/release 公共模板方法

park/unpark 实现阻塞和唤醒

CLH队列结构:

//每个节点结构
static final class Node {
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    volatile int waitStatus;
}

waitStatus:

0 → 正常

SIGNAL → 释放锁时需要唤醒该节点

CANCELLED → 超时或中断

CONDITION → Condition 专用状态

AQS内部可以理解为两个队列:

  • 同步队列(Synch Queue)——锁竞争的队列

  • 等待队列(Condition Queue)——对应 Condition.await() 的队列

ReentrantLock

ReentrantLock 的本质是基于 AQS 的可重入互斥锁,使用 CAS + CLH 队列 + LockSupport 实现。

结构:

public class ReentrantLock implements Lock {
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer { ... }
    static final class NonfairSync extends Sync { ... }
    static final class FairSync extends Sync { ... }
}

锁的逻辑其实全部在 Sync(继承 AQS) 中

对于 ReentrantLock来说,AQS内部的state表示锁状态,为0时说明锁空闲,不为0时说明锁已被某线程持有(值 = 重入次数)

默认为非公平锁,允许刚来的现在CAS抢锁:

final void lock() {
    if (compareAndSetState(0, 1)) {  // 1. 直接用 CAS 抢锁(插队)
        setExclusiveOwnerThread(Thread.currentThread());
    } else {
        acquire(1); // 2. CAS 失败 → 进入 AQS acquire 流程
    }
}

也可以指定为公平锁,不允许插队:

final void lock() {
    acquire(1);
}

acquire方法作用为获取独占锁:

acquire(arg):
1. 尝试 tryAcquire()  → 尝试获取锁(子类 ReentrantLock 实现)
2. 失败 → 将当前线程包装为 Node 加入 CLH 同步队列尾部
3. 自旋(for (;;))检查前驱节点是否是头节点
4. 如果是 → 再次 tryAcquire()
5. 如果还失败 → park() 阻塞线程
6. 被 unpark() 唤醒后继续循环

ReentrantLock的解锁流程:

release(arg):
1. tryRelease(arg)  → state--,若 state == 0 则释放锁成功
2. 若锁彻底释放成功 → 唤醒队列头节点的后继节点(unpark)

Condition

Condition为多条件队列,是 ReentrantLock 的“等待队列机制”,让你可以让线程在锁内部“有条件地等待和唤醒”。

synchronized 的 wait/notify 是“单一等待队列”,无法有条件的唤醒某类线程,而一个 Lock 可以创建多个 Condition,这就等于同一个锁,拥有多个独立的等待队列,每个队列对应一种“条件状态”

Condition的三个核心方法:

await():把当前线程放入该 Condition 的队列,并释放锁 → 阻塞

signal():唤醒 Condition 队列中的一个线程(不会立即拿到锁,需要参与锁竞争)

signalAll():唤醒该条件队列中的所有线程

await() 步骤:

1 线程被封装成 Node(状态为 CONDITION)
2 加入 Condition 的等待队列
3 释放锁(state = 0)
4 线程被 park() 阻塞
5 被 signal 唤醒后,从 Condition 队列移动到同步队列
6 再次竞争锁
7 获取锁后 await 返回

ReentrantReadWriteLock

读写锁,设计思想为读之间不互斥,写与写互斥,读与写互斥,适用于“读多写少”的高并发场景。

读锁不升级为写锁,写锁可以降级为读锁

StampedLock

是JDK8 引入的新型锁,有悲观读锁、乐观读锁、写锁三种模式。

写锁(Write Lock):独占、阻塞,一次只允许一个线程写

悲观读锁(Pessimistic Read Lock):共享、阻塞,读读不互斥,但读写互斥

乐观读锁(Optimistic Read):不加锁、不阻塞,读的时候“假设没人写”,用完再校验

其中乐观读锁性能高,是其最大的亮点:

long stamp = lock.tryOptimisticRead();
// 使用共享数据
if (!lock.validate(stamp)) {
    // 失败,升级为悲观读锁
    stamp = lock.readLock();
    try {
    } finally {
        lock.unlockRead(stamp);
    }
}

乐观读不阻塞写线程,但必须 validate() 确认数据是否被写线程修改,适用于数据读多写少,对实时一致性要求不像金融系统那么高的场景。

但 StampedLock 是不可重入的,stamp 代表一次“访问许可”,如果可重入会破坏版本校验语义。

7. CAS 与原子类(Atomic 包)

什么是CAS

CAS(Compare-And-Swap) 是一种 无锁原子操作:比较内存中的值是否等于期望值,如果是,则更新为新值;否则失败。

比较 + 修改 是一个不可分割的原子操作,这是 CAS 的核心

CAS 为什么原子

CAS 并不是 Java 发明的,而是 CPU 指令级支持:

  • x86:LOCK CMPXCHG

  • ARM:LDXR / STXR

  • RISC-V:LR / SC

这些指令保证:

  • 在执行 CAS 时,CPU 会锁住对应内存地址

  • 或使用缓存一致性协议(MESI)

  • 确保多核下只有一个 CPU 能成功修改

Java 的 CAS 实际是 JVM 调用 CPU 的原子指令。

CAS三大问题

ABA 问题:

线程 A 读取到值 A,线程 B 将 A 改成 B,又改回 A,线程 A CAS 成功,但“期间发生过修改”。

解决:使用版本号/AtomicStampedReference/AtomicMarkableReference

自旋开销问题:

CAS 在竞争激烈时,大量线程 CAS 失败,无限自旋,CPU 飙高

只能保证一个变量的原子性:

CAS 只能作用于一个内存地址,无法保证多个变量的一致性,无法实现复杂临界区,这时需要锁。

CAS vs synchronized

维度

CAS

synchronized

是否阻塞

上下文切换

适合场景

低冲突、简单操作

高冲突、复杂逻辑

可组合性

ABA 问题

Atomic包

基础原子类:

用途

AtomicInteger

原子 int

AtomicLong

原子 long

AtomicBoolean

原子 boolean

AtomicReference

原子对象引用

版本号原子类(解决 ABA):

作用

AtomicStampedReference

引用 + 版本号

AtomicMarkableReference

引用 + 标记位

数组原子类:

AtomicIntegerArray

AtomicLongArray

AtomicReferenceArray

字段更新器(反射 + CAS):

AtomicIntegerFieldUpdater

AtomicLongFieldUpdater

AtomicReferenceFieldUpdater

为什么Atomic保证原子性

Atomic 类内部最终都会调用CAS方法,CAS 操作是硬件保证的原子操作,多线程并发更新时,只有一个线程 CAS 成功,失败的线程会重试(自旋)。

AtomicInteger如何工作

以 incrementAndGet 为例:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

底层逻辑:

do {
    old = value;
    new = old + 1;
} while (!CAS(value, old, new));

失败就自旋重试,直到成功。

LongAdder、DoubleAdder

LongAdder 是高并发下 AtomicLong 的升级优化版,是 Atomic 包的一员,它通过分段 CAS 解决 AtomicLong 在高并发下的竞争问题,是高吞吐计数的优化方案。

LongAdder 比 AtomicLong 快:

AtomicLong:

  • 所有线程 CAS 同一个变量

  • 高并发下冲突严重

LongAdder:

  • 分段计数(Striped Cells)

  • 每个线程更新不同的 Cell

  • 最后 sum() 汇总

LongAdder 用空间换时间,牺牲实时精确性换高并发性能

8. 并发容器

早期线程安全集合:Collections.synchronizedXxx

早期的线程安全集合(比如 Vector、Hashtable,或者 Collections.synchronizedList)都依赖synchronized 整体加锁(方法级别 + JVM Monitor)

这种整体加锁的方法有很多缺点,锁的粒度大,整个对象是一个大锁,只有一个线程能访问。没有读写分离,读写操作是互斥的,且对高并发完全不适用。每个操作都要获取监视器锁,开销大。

JUC 并发容器

JUC(java.util.concurrent)中的容器采用现代并发结构:

分段锁 / Node 局部锁

无锁结构(CAS、自旋)

读写分离

写时复制(CopyOnWrite)

阻塞队列(BlockingQueue)

JUC 的并发容器主要分为 5 类:

ConcurrentHashMap

CopyOnWriteArrayList / CopyOnWriteArraySet

ConcurrentLinkedQueue / ConcurrentLinkedDeque

BlockingQueue 系列

并发跳表:ConcurrentSkipListMap / SkipListSet

ConcurrentHashMap

在JDK7和JDK8中是两种方案:

JDK7

JDK8

分段锁(Segment 数组 + ReentrantLock)

数组 + 链表 + 红黑树

Segment 锁粒度较大

更细粒度(Synchronize + CAS)

并发度由 Segments 数决定

无分段,性能更高

JDK7结构:

ConcurrentHashMap
 └── Segment[] segments        ← 每个 Segment 一个锁
      └── HashEntry[] table

整个 Map 被分成 16 个 Segment(默认),每个 Segment 内独立加锁(ReentrantLock),写操作锁 Segment,读操作无锁(volatile 读)

缺点是Segment 数量固定,并发程度有限,扩容时锁住整个 Segment,性能也一般。

JDK8结构:

Node[] table  ← 数组 + 链表 + 树

使用 CAS + synchronized 替代 ReentrantLock,将链表树化优化查找。仅对单个桶位(bin)加锁,而不是全段加锁,扩容采用协作式迁移(多个线程一起搬迁 Node)

put/get操作流程如下:

put():

1. table 未初始化 → CAS 初始化

2. 根据 hash 定位桶位 i

3. 如果 table[i] 为空 → CAS 插入新 Node(无锁)

4. 如果非空 → synchronized 锁住这个 bin

5. 链表插入(或树插入)

6. 如果长度超过 8 → 树化(红黑树

get():

  1. 计算 hash,定位 table[i]

  2. 如果链表则遍历

  3. 如果是红黑树则树查找

  4. 因为节点的 value / next 是 volatile,因此无锁也能安全读

HashMap可以存储null值,但ConcurrentHashMap不能,原因在于null 值会在并发下让 get() 的返回造成二义性,举例说明:

A线程:

V v = map.get("a"); // 返回 null

B线程:

map.remove("a");

A线程拿到的null值可能是key 从来不存在,可能是key 存在但 value 是 null,也可能是key 存在,但在你调用 get 之后被删了。

CopyOnWriteArrayList / CopyOnWriteArraySet

写时复制容器,原理为写操作时复制整个数组,写完后替换引用;读操作无锁。

写操作:

lock.lock();
try {
    Object[] newArr = Arrays.copyOf(oldArr);
    newArr[i] = x;
    array = newArr;
} finally {
    lock.unlock();
}

读操作:

return array[index];  // 无锁读

这种方案的优点是读极快(无锁),可以避免读写冲突,适合读远多于写的场景

但是写代价巨大,内存开销大

ConcurrentLinkedQueue

无锁队列,也是经典 CAS 队列,结构基于 Michael-Scott Lock-Free Queue(M&S 算法)

特点是入队 CAS 不加锁,出队 CAS 不加锁,链表结构,不扩容

出队/入队只会 CAS 头/尾引用:

CAS tail
CAS head

优点是无锁,性能极高,无阻塞

缺点是非有界队列,可能 OOM。不是严格 FIFO,在极端情况下有轻微延迟

BlockingQueue 系列

是生产者消费者框架的核心,这些队列内部都使用 锁 + Condition 来实现阻塞行为。

主要有:

队列

特点

ArrayBlockingQueue

数组、有界、单锁(ReentrantLock)

LinkedBlockingQueue

链表、有界(默认无界)、双锁(putLock/takeLock)

DelayQueue

延时队列

PriorityBlockingQueue

优先级队列

SynchronousQueue

不存储元素(直接交付)

其中,

ArrayBlockingQueue 的特点:

单 ReentrantLock + 两个 Condition

put 阻塞直到不满

take 阻塞直到不空

LinkedBlockingQueue 的特点:

用两个锁提高吞吐量(putLock、takeLock)

读写互不影响。

ConcurrentSkipListMap

跳表结构,支持有序 Map,比 TreeMap 线程安全。基于随机层级,非阻塞 CAS 实现。适合需要排序的高并发场景如排行榜等等。

跳表为什么能做成线程安全?

跳表是“多层有序链表”的结构,天然适合无锁化(Lock-Free / CAS)操作,每个节点之间是有序链表结构,可以只更新局部节点,不需要全局锁。

插入/删除只影响局部指针,删除使用标记法无需立即物理删除,链表结构不存在树旋转,可以用 CAS 更新 next 指针,因此可以实现 Lock-Free 的高并发结构。

跳表 vs 红黑树?

时间复杂度一致,查找添加删除都为O(log n),细微的差别在跳表常数项较小,而红黑树由于严格平衡的结构在极端情况下稍快

跳表并发结构简单,易实现无锁,结构修改简单。红黑树实现无锁需要树旋转,结构修改复杂。

跳表性能很好,红黑树略快但差距极小。

跳表比红黑树占用更多内存,有多层索引且每个节点有多个 next 指针,使用空间换时间。

跳表适合并发,是因为插入和删除只需要调整局部指针,不会破坏全局结构,因此可以基于 CAS 实现 lock-free;而红黑树需要旋转和颜色调整,多个节点之间强耦合,很难做到无锁化,所以没有 ConcurrentRedBlackTree。

跳表空间占用更高,但并发性能远优,红黑树则单线程性能略好但难并发化。

9. 线程池

荐读:Java线程池实现原理及其在美团业务中的实践 - 美团技术团队

设计目的

线程池的设计目标是三个:

降低线程创建和销毁的成本(线程是重量级资源)

控制并发数量(避免 OOM、过载)

任务调度能力(任务队列 + 拒绝策略)

图2 ThreadPoolExecutor运行流程

ThreadPoolExecutor 的七大核心参数

ThreadPoolExecutor 的构造函数:

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

corePoolSize:核心线程数。核心线程默认不会被回收,即使空闲也会一直存在。线程池创建之后不会立刻创建核心线程,只有任务提交才会创建

maximumPoolSize:最大线程数。核心线程数 < 活动线程数 ≤ 最大线程数。

keepAliveTime:非核心线程存活时间。只有当队列满时才会创建非核心线程,默认只有非核心线程才会受此参数影响。

unit:时间单位。用来指定 keepAliveTime 的时间单位。

workQueue:任务队列。队列的选择决定线程池行为,常用队列类型有SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue

队列类型

特点

SynchronousQueue

不存储任务,必须立即交付

LinkedBlockingQueue

无界队列(默认)

ArrayBlockingQueue

有界队列

PriorityBlockingQueue

带优先级的队列

threadFactory:线程工厂。用于创建线程,一般用来自定义线程名称,设置 daemon/priority 与自定义 UncaughtExceptionHandler

handler:拒绝策略。当队列满、线程数达到 maximumPoolSize 时执行RejectedExecutionHandler.rejectedExecution(),四种内置拒绝策略:

策略

行为

AbortPolicy

直接抛异常(默认)

CallerRunsPolicy

在调用线程执行任务(能起到削峰作用)

DiscardPolicy

静默丢弃任务

DiscardOldestPolicy

丢弃队列最旧的任务

线程池执行流程

当调用:

executor.execute(task);

ThreadPoolExecutor 会按如下顺序处理:

步骤 1:如果当前线程数 < corePoolSize → 创建新线程执行任务。

步骤 2:如果当前线程数 ≥ corePoolSize → 尝试将任务放入队列

步骤 3:如果队列已满 → 尝试创建新线程(非核心线程)

步骤 4:如果线程数达到 maximumPoolSize → 调用拒绝策略 handler

图4 任务调度流程

内部工作线程

ThreadPoolExecutor 内部用 Worker 包装线程:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable {

Worker 继承 AQS,本身是一把锁,保存一个 firstTask,其run() 方法执行任务循环

run() 方法:

while (任务 != null 或 从队列取任务 != null) {
    执行任务
}

线程池状态

图3 线程池生命周期

ThreadPoolExecutor 的 ctl(32bit)高 3 位表示状态:

状态

描述

RUNNING

接受新任务 + 处理队列任务

SHUTDOWN

不接受新任务,但执行队列任务

STOP

中断正在执行的任务,丢弃队列任务

TIDYING

全部任务执行完,线程清理中

TERMINATED

线程池完全退出

任务队列的选择

SynchronousQueue(不存任务):

场景:缓存线程池(Executors.newCachedThreadPool)

行为:每提交一个任务,都需要找到一个空闲线程,否则创建新线程。

缺点:容易创建大量线程 → CPU 飙高

LinkedBlockingQueue(无限队列):

场景:默认 newFixedThreadPool

问题:

队列无限增长

maximumPoolSize 失效

容易导致 OOM

ArrayBlockingQueue(有界队列):

推荐生产使用的最佳选择

可以通过队列长度做流量控制。

PriorityBlockingQueue(带优先级):

用于任务按照优先级执行

注意任务必须实现 Comparable

配置线程池

公式来自《Java Concurrency in Practice》

对于CPU 密集型任务,为了避免上下文切换:

线程数 = CPU核心数 + 1

对于 IO 密集型任务:

线程数 ≈ CPU核心数 × IO等待时间/CPU执行时间

一般经验(大部分线程在等待 IO):

线程数 ≈ CPU 核数 × 2 ~ 4

对于混合型任务:

拆分成 CPU 部分 + IO 部分,分别建线程池。

创建线程池的方式

直接使用 ThreadPoolExecutor

ExecutorService pool = new ThreadPoolExecutor(
    4,
    8,
    60,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

参数可控,行为可预测,可避免 OOM,符合阿里开发规范,是生产环境的首选

使用 Executors 工厂方法:

Executors.newFixedThreadPool(n);
Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();
Executors.newScheduledThreadPool(n);

方法

隐患

newFixedThreadPool

无界队列 → OOM

newCachedThreadPool

线程数无限 → CPU 飙高

newScheduledThreadPool

无界延时队列

不推荐在生产环境使用 Executors 创建线程池

ScheduledThreadPoolExecutor(定时 / 延时任务):

//示例
ScheduledExecutorService scheduler =
    Executors.newScheduledThreadPool(2);
//或者是
ScheduledThreadPoolExecutor scheduler =
    new ScheduledThreadPoolExecutor(2);

其实是 ThreadPoolExecutor 的子类,用于定时 / 延时任务、心跳检测和超时处理

ForkJoinPool(并行计算线程池):

ForkJoinPool pool = new ForkJoinPool();

用于工作窃取(Work-Stealing),适合 CPU 密集型,Java 8 的 CompletableFuture 默认使用它

另外还可以自定义 ThreadFactory 创建线程池:

ThreadFactory factory = r -> {
    Thread t = new Thread(r);
    t.setName("biz-thread-" + t.getId());
    t.setDaemon(false);
    return t;
};

ExecutorService pool = new ThreadPoolExecutor(
    4, 8, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    factory,
    new ThreadPoolExecutor.AbortPolicy()
);

为什么不推荐 Executors 创建线程池

上文已经提到了,复习一下:

Executors 的默认实现存在严重隐患,不推荐用 Executors,推荐显式使用 ThreadPoolExecutor:

方法

问题

newFixedThreadPool

使用无界队列 → OOM

newCachedThreadPool

最大线程数 = Integer.MAX → 线程爆炸

newScheduledThreadPool

同样使用无界队列

10. 并发编程中的设计模式

生产者–消费者模型

生产者–消费者模型的定义为:生产者生产数据,将它放入缓冲区;消费者从缓冲区获取数据处理。

Java 实现方式有三种:

① synchronized + wait/notify(基础版):

synchronized(lock) {
    while(full) lock.wait();
    put();
    lock.notifyAll();
}

缺点:

易写错

wait/notify 操作不精确

只有一个等待队列(WaitSet)

② BlockingQueue(推荐)

BlockingQueue 内部用ReentrantLock、Condition notEmpty、Condition notFull 实现阻塞队列。

例如:

BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();

生产者:

queue.put(x); // 队列满时阻塞

消费者:

queue.take(); // 队列空时阻塞

这是 Java 生产者–消费者的最推荐实现。

③ Disruptor(高性能场景)

用于交易撮合、日志处理、高频风控等场景。

底层是 RingBuffer + CAS + 内存屏障,比 BlockingQueue 快很多。

Future / Promise / CompletableFuture

传统 Future 的问题:

Future<String> f = executor.submit(task);
String result = f.get(); // 阻塞

Future 无法组合多个任务,无法链式执行,无法非阻塞获取结果也不能捕获异常

CompletableFuture (Java 8):

CompletableFuture.supplyAsync(() -> task1())
                 .thenApply(result1 -> task2(result1))
                 .thenAccept(finalResult -> System.out.println(finalResult));

特点:

  • 完全异步(基于 ForkJoinPool)

  • 提供 50+ API:thenApply、handle、exceptionally、allOf、anyOf…

  • 解决回调地狱

  • 异步执行流水线(任务依赖链)

CompletableFuture 与回调函数的区别?

CompletableFuture 提供声明式异步链,不依赖深层嵌套回调,同时内置异常传播机制。

线程池模式

线程池本身就是一种并发设计模式,其理念为用线程池复用线程,减少创建成本,并通过队列控制系统压力。

Java 的 ThreadPoolExecutor 实现了完整的线程池模式:

  • Worker 线程复用

  • 任务队列

  • 拒绝策略

  • 队列 + 线程数控制负载

  • 提供异步执行模型

Actor 模式(Akka、Erlang 模型)

Java 没有原生 Actor,但 Akka 实现了它,其核心思想为每个 Actor 都有自己的消息队列。
所有线程之间不共享内存,靠消息通信。

优点在于无锁编程,天然解决并发冲突,高扩展性

缺点是编程模型较复杂,在Java 中依赖 Akka 框架

应用:

  • 分布式系统

  • 电信系统(Erlang)

  • 大规模聊天系统

Reactor 模式(Netty 的核心)

核心思想是一个(或多个)线程等待事件到来,然后分发到不同的处理器(Handler)。

用于 IO 多路复用(事件驱动 IO)

当你调用:

NioEventLoopGroup bossGroup = new NioEventLoopGroup();

你就在使用 Reactor 模式。

过程:

  1. Reactor 监听事件(accept、read、write)

  2. 事件发生 → Reactor 分发给处理器

  3. Handler 执行具体逻辑

Netty 是经典 Reactor + 多线程版本。

应用:

高并发网络服务

聊天服务器

RPC 框架(Netty、Dubbo)

Fork/Join 模式(Java 7 引入的并行计算框架)

Fork/Join 的目标是将大任务拆分为多个小任务并行执行,再合并结果。

使用 RecursiveTask:

class Fibonacci extends RecursiveTask<Integer> {
    protected Integer compute() {
        if(n <= 1) return n;
        Fibonacci f1 = new Fibonacci(n-1);
        f1.fork();
        Fibonacci f2 = new Fibonacci(n-2);
        return f2.compute() + f1.join();
    }
}

底层使用:

工作窃取(Work Stealing)算法

每个线程都有自己的任务队列

空闲线程会偷别人的任务

用途:

CPU 密集型任务

并行计算(大数组求和、大文件处理)

Disruptor(高性能事件驱动模型)

Lmax 的 Disruptor 曾经打破交易系统的性能记录。

用途:

超低延迟系统

高频交易

银行风控

日志异步处理

核心机制:

RingBuffer(环形数组)

CAS + 内存屏障

无锁生产者消费者模型

快到能替代队列

Disruptor 为什么比 BlockingQueue 快?

避免 GC

自旋 CAS

内存连续、CPU 缓存友好

单 Producer 单 Consumer 情况性能极致

11. 并发 bug 与调优

并发的典型问题

死锁

两个或多个线程互相等待对方释放资源,导致永远无法继续执行。

四个必要条件:互斥(Mutual Exclusion)、占有并等待(Hold and Wait)、不可抢占(Non-preemption)、循环等待(Circular Waiting)

synchronized(A) {
    synchronized(B) { ... }
}

synchronized(B) {
    synchronized(A) { ... }
}

避免:

加锁顺序一致(最有效的方法)

尽量减少锁的粒度

使用 tryLock(timeout)

使用死锁检测工具

排查:

使用 jstack

jstack <pid> | grep -A20 "Found one Java-level deadlock"

活锁

线程没有阻塞,也没有死锁,但彼此“谦让”,导致任务一直无法完成。

解决:

引入随机时间退避(Backoff)

使用有边界的重试机制

饥饿

线程长期得不到 CPU 时间或资源(例如被高优先级线程长期压制)

解决:

使用公平锁(ReentrantLock(true))

避免线程优先级控制系统行为

不要在锁内执行耗时操作

伪共享

两个线程修改两个不同变量,但这两个变量位于同一个缓存行(cache line),导致 CPU 不断缓存失效,性能极差。

解决方法

使用 @Contended(JDK 8 需加 -XX:-RestrictContended)

将变量分散到不同缓存行

使用 LongAdder(内部已经避免伪共享)

调度延迟 / 上下文切换过多

CPU 高但程序处理能力低,Thread Dump 显示大量 Runnable,perf 显示 schedule() 开销大

原因可能是线程数远高于 CPU 核数/频繁阻塞与唤醒(锁竞争)/使用重量级锁

解决:

控制线程池线程数

减少锁竞争

使用无锁结构(CAS / Disruptor / LongAdder)

内存可见性问题

原因:

缓存导致线程读到旧值

指令重排导致读取时机不确定

缺乏同步手段(volatile / synchronized)

解决:

使用 volatile(状态标志)

synchronized / Lock(进入前清空本地缓存)

使用并发容器代替普通容器

并发问题如何排查

jstack

可以定位死锁、阻塞、等待

查看 Java 线程栈:

jstack <pid>

关键看:

  • BLOCKED(锁竞争)

  • WAITING(等待 Condition)

  • TIMED_WAITING

  • RUNNABLE(可能在忙循环)

Arthas(阿里巴巴神器)

常用命令:

thread -b    # 查看哪个线程阻塞了
thread -n 10 # 查看最耗 CPU 线程
monitor      # 监控方法调用时间
trace        # 方法调用链

top + perf(查看系统层性能瓶颈)

比如:

perf top

可以看到 CPU 在跑什么:

  • 多在 futex → 说明锁竞争严重

  • 多在 schedule → 上下文切换太多

  • 多在 memcpy → 拷贝数据太多

  • 多在 GC → GC 压力大

并发调优

优化 1:减少锁竞争

方法:

  • 缩小锁的粒度(Lock Splitting / Fine-Grained Locking)

  • 减少锁的持有时间(不要在锁里做 IO)

  • 使用读写锁 ReentrantReadWriteLock

  • 使用无锁结构(ConcurrentHashMap、CAS)

  • 使用写时复制(CopyOnWrite)适合读多写少

优化 2:避免不必要的同步

场景:

synchronized(list) {
    for (...) { ... }
}

问题:持有锁时间过长
做法:拆成两段:

snapshot = new ArrayList(list); // 快照
// 无锁处理

优化 3:选择合适的线程池

CPU密集型:

线程数 = CPU核数 + 1

IO密集型:

线程数 = CPU核心数 * (1 + IO耗时/CPU耗时)
≈ 核心数 * 2~4

优化 4:减少上下文切换

  • 减少线程数量

  • 避免频繁阻塞

  • 使用协程(虚拟线程、Kotlin coroutine)

  • 使用 Disruptor 或无锁队列

优化 5:提高缓存命中率(避免伪共享)

  • padding 填充缓存行

  • 使用 @Contended

  • 使用 LongAdder 替代 AtomicLong

调优总结

死锁来自循环等待,可以通过固定加锁顺序避免;活锁来自过度让步,需要随机退避;饥饿来自调度不公平,可以用公平锁或避免长时间占锁解决。伪共享会导致 CPU 缓存行抖动,使用 @Contended 或 LongAdder 可避免。并发调优本质是减少锁竞争、减少线程切换、提高缓存命中率。