第一章 线程基础
1.1 进程与线程的概念
进程的定义和特点
**进程(Process)**是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间,包括代码段、数据段、堆栈段等。
进程的特点:
- 独立性:进程之间相互独立,一个进程的崩溃不会影响其他进程
- 资源隔离:每个进程拥有独立的地址空间,进程间不能直接访问对方的内存
- 开销大:进程的创建、切换、销毁都需要较大的系统开销
- 通信复杂:进程间通信需要通过IPC(Inter-Process Communication)机制,如管道、信号、共享内存等
线程的定义和特点
**线程(Thread)**是CPU调度的基本单位,是进程内的一个执行流。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。
线程的特点:
- 轻量级:线程的创建、切换、销毁开销比进程小得多
- 共享资源:同一进程内的线程共享进程的内存空间、文件描述符等资源
- 并发执行:多个线程可以并发执行,提高程序的执行效率
- 通信简单:线程间可以直接通过共享内存进行通信,但需要同步机制保证线程安全
进程与线程的区别
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 拥有独立的地址空间和资源 | 共享进程的地址空间和资源 |
| 创建开销 | 大(需要分配独立的内存空间) | 小(共享进程资源) |
| 切换开销 | 大(需要切换地址空间) | 小(只需切换上下文) |
| 通信方式 | 需要IPC机制(管道、信号等) | 可直接通过共享内存通信 |
| 独立性 | 完全独立,一个进程崩溃不影响其他进程 | 相互影响,一个线程崩溃可能导致整个进程崩溃 |
| 数量 | 系统资源有限,进程数量较少 | 一个进程可以创建大量线程 |
多进程 vs 多线程
多进程的优势:
- 更好的隔离性,一个进程的崩溃不会影响其他进程
- 可以利用多核CPU,实现真正的并行
- 适合需要高稳定性的场景
多线程的优势:
- 创建和切换开销小,性能更高
- 线程间通信简单,数据共享方便
- 适合需要频繁通信和协作的场景
选择建议:
- 需要高隔离性、高稳定性 → 选择多进程
- 需要频繁通信、共享数据 → 选择多线程
- 现代应用通常采用多线程 + 进程隔离的混合模式
1.2 Java线程的创建方式
方式一:继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
特点:
- 简单直接,适合简单的线程任务
- Java是单继承,继承Thread后无法继承其他类
- 不推荐使用,因为耦合度高
方式二:实现Runnable接口(推荐)
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
特点:
- 实现接口,可以继承其他类,更灵活
- 符合面向接口编程的原则
- 任务和线程分离,耦合度低
- 推荐使用
方式三:实现Callable接口
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "任务执行完成: " + Thread.currentThread().getName();
}
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
// 获取返回值
String result = futureTask.get();
System.out.println(result);
}
}
特点:
- 可以有返回值
- 可以抛出异常
- 需要配合FutureTask使用
- 适合需要返回结果的异步任务
方式四:使用线程池创建
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
System.out.println("线程池执行任务: " + Thread.currentThread().getName());
});
executor.shutdown();
}
}
特点:
- 线程复用,性能更好
- 统一管理线程生命周期
- 控制并发数量
- 生产环境推荐使用
方式五:Lambda表达式创建
public class LambdaThread {
public static void main(String[] args) {
// 方式1:使用Runnable的Lambda表达式
Thread thread1 = new Thread(() -> {
System.out.println("Lambda线程: " + Thread.currentThread().getName());
});
thread1.start();
// 方式2:直接使用线程池
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(() -> {
System.out.println("线程池Lambda: " + Thread.currentThread().getName());
});
executor.shutdown();
}
}
特点:
- 代码简洁
- 适合简单的任务
- Java 8+支持
1.3 线程的生命周期
Java线程有6种状态,定义在Thread.State枚举中:
NEW(新建)
线程被创建但尚未启动的状态。
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // NEW
RUNNABLE(可运行)
线程正在JVM中执行,但可能正在等待操作系统分配CPU时间片。
Thread thread = new Thread(() -> {
while(true) {
// 运行中或等待CPU时间片
}
});
thread.start();
System.out.println(thread.getState()); // RUNNABLE
注意: RUNNABLE状态包括:
- Running:正在执行
- Ready:就绪,等待CPU调度
BLOCKED(阻塞)
线程被阻塞,等待获取监视器锁(monitor lock)。
public class BlockedExample {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(5000); // 持有锁5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
// 等待thread1释放锁
}
});
thread1.start();
Thread.sleep(100); // 确保thread1先获取锁
thread2.start();
Thread.sleep(100);
System.out.println(thread2.getState()); // BLOCKED
}
}
WAITING(等待)
线程无限期等待另一个线程执行特定操作。
public class WaitingExample {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
synchronized (lock) {
try {
lock.wait(); // 进入WAITING状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(100);
System.out.println(thread.getState()); // WAITING
}
}
进入WAITING状态的方法:
Object.wait()- 等待被notify/notifyAll唤醒Thread.join()- 等待目标线程执行完成LockSupport.park()- 等待被unpark唤醒
TIMED_WAITING(超时等待)
线程在指定时间内等待。
public class TimedWaitingExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(5000); // 进入TIMED_WAITING状态
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
Thread.sleep(100);
System.out.println(thread.getState()); // TIMED_WAITING
}
}
进入TIMED_WAITING状态的方法:
Thread.sleep(long millis)Object.wait(long timeout)Thread.join(long millis)LockSupport.parkNanos()LockSupport.parkUntil()
TERMINATED(终止)
线程执行完成或异常终止。
Thread thread = new Thread(() -> {
System.out.println("执行完成");
});
thread.start();
thread.join(); // 等待线程执行完成
System.out.println(thread.getState()); // TERMINATED
状态转换图
NEW
|
| start()
↓
RUNNABLE ←──────────┐
| |
| wait() | notify()/notifyAll()
↓ |
WAITING ──────────┘
|
| sleep(timeout)/wait(timeout)/join(timeout)
↓
TIMED_WAITING
|
| 获取锁失败
↓
BLOCKED
|
| 获取到锁
↓
RUNNABLE
|
| run()方法执行完成或异常
↓
TERMINATED
1.4 线程的基本操作
start()方法
启动线程,使线程进入RUNNABLE状态。
Thread thread = new Thread(() -> {
System.out.println("线程执行");
});
thread.start(); // 启动线程
// thread.start(); // 错误!不能重复调用start()
注意:
start()只能调用一次,重复调用会抛出IllegalThreadStateExceptionstart()会创建新的线程,而run()只是普通方法调用
run()方法
线程的执行体,包含线程要执行的代码。
Thread thread = new Thread(() -> {
System.out.println("run方法执行");
});
thread.run(); // 直接调用run(),不会创建新线程,在当前线程执行
thread.start(); // 调用start(),创建新线程执行run()方法
start() vs run():
start():创建新线程,异步执行run():普通方法调用,同步执行
sleep()方法
让当前线程休眠指定时间,进入TIMED_WAITING状态。
try {
Thread.sleep(1000); // 休眠1秒
System.out.println("休眠结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
特点:
- 不释放锁
- 可能抛出
InterruptedException - 时间到了自动唤醒
yield()方法
提示调度器当前线程愿意让出CPU时间片,但调度器可以忽略这个提示。
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
Thread.yield(); // 让出CPU时间片
}
});
thread.start();
注意:
yield()只是提示,不保证一定会让出CPU- 适合用于调试和测试,生产环境不常用
join()方法
等待目标线程执行完成。
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("thread1执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
thread1.join(); // 等待thread1执行完成
System.out.println("thread2执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
重载方法:
join()- 无限期等待join(long millis)- 等待指定时间join(long millis, int nanos)- 等待指定时间(纳秒精度)
interrupt()方法
中断线程,设置线程的中断标志位。
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 捕获异常后,中断标志位被清除
System.out.println("线程被中断");
Thread.currentThread().interrupt(); // 重新设置中断标志
break;
}
}
});
thread.start();
Thread.sleep(3000);
thread.interrupt(); // 中断线程
注意:
interrupt()只是设置中断标志位,不会强制停止线程- 线程需要检查中断标志位并自行退出
- 如果线程在阻塞状态(sleep、wait等),会抛出
InterruptedException
isInterrupted()方法
检查线程的中断标志位,不会清除标志位。
Thread thread = new Thread(() -> {});
thread.start();
thread.interrupt();
System.out.println(thread.isInterrupted()); // true
System.out.println(thread.isInterrupted()); // true(标志位还在)
interrupted()方法
检查当前线程的中断标志位,会清除标志位。
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted()); // true
System.out.println(Thread.interrupted()); // false(标志位被清除)
setDaemon()守护线程
设置线程为守护线程,当所有非守护线程结束时,JVM会自动退出。
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("守护线程运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
Thread.sleep(3000);
System.out.println("主线程结束,JVM退出");
// 守护线程也会随之结束
特点:
- 守护线程不能独立存在,必须依赖非守护线程
- 适合执行后台任务,如垃圾回收、监控等
- 必须在
start()之前设置
setPriority()线程优先级
设置线程的优先级(1-10),数字越大优先级越高。
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("高优先级: " + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("低优先级: " + i);
}
});
thread1.setPriority(Thread.MAX_PRIORITY); // 10
thread2.setPriority(Thread.MIN_PRIORITY); // 1
thread1.start();
thread2.start();
优先级常量:
Thread.MIN_PRIORITY = 1Thread.NORM_PRIORITY = 5(默认)Thread.MAX_PRIORITY = 10
注意:
- 优先级只是提示,操作系统可能忽略
- 不同操作系统对优先级的处理不同
- 不推荐依赖优先级来保证程序正确性
第二章 线程安全基础
2.1 什么是线程安全
线程安全的定义
**线程安全(Thread Safety)**是指当多个线程访问同一个对象时,不需要额外的同步机制,程序仍能正确执行,并且结果符合预期。
Brian Goetz在《Java并发编程实战》中的定义:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
竞态条件(Race Condition)
竞态条件是指程序的执行结果依赖于线程执行的相对时序。
public class RaceCondition {
private int count = 0;
public void increment() {
count++; // 不是原子操作
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
RaceCondition rc = new RaceCondition();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
rc.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
rc.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果: " + rc.getCount());
// 期望20000,实际可能小于20000
}
}
原因分析:
count++不是原子操作,实际包含三个步骤:
- 读取count的值
- 将count加1
- 将新值写回count
两个线程可能同时读取到相同的值,导致结果不正确。
数据竞争(Data Race)
数据竞争是指多个线程在没有同步的情况下,同时访问同一个共享变量,并且至少有一个线程在写。
public class DataRace {
private boolean flag = false;
private int value = 0;
// 线程1
public void writer() {
value = 42;
flag = true; // 可能被重排序
}
// 线程2
public void reader() {
if (flag) {
System.out.println(value); // 可能看到value=0
}
}
}
可见性问题
可见性是指一个线程对共享变量的修改,能够及时被其他线程看到。
public class VisibilityProblem {
private boolean running = true; // 没有volatile修饰
public void start() {
new Thread(() -> {
while (running) {
// 可能永远循环,因为看不到running的变化
}
System.out.println("线程结束");
}).start();
}
public void stop() {
running = false; // 修改可能对其他线程不可见
}
public static void main(String[] args) throws InterruptedException {
VisibilityProblem vp = new VisibilityProblem();
vp.start();
Thread.sleep(1000);
vp.stop(); // 可能无法停止线程
}
}
原因:
- CPU缓存:每个线程可能在自己的CPU缓存中保存变量的副本
- 指令重排序:编译器和CPU可能重排序指令
解决方案:
private volatile boolean running = true; // 使用volatile保证可见性
原子性问题
原子性是指一个操作要么全部执行,要么都不执行,不会被打断。
public class AtomicityProblem {
private int count = 0;
public void increment() {
count++; // 不是原子操作
}
// 原子操作示例
public synchronized void incrementSync() {
count++; // 现在是原子操作
}
}
非原子操作示例:
count++- 读取、修改、写入三步count = count + 1- 同上obj.field = obj.field + 1- 同上
原子操作:
- 基本类型的赋值(long和double在32位JVM上除外)
- volatile变量的读写
- synchronized块内的操作
有序性问题
有序性是指程序执行的顺序按照代码的先后顺序执行。
public class OrderingProblem {
private int a = 0;
private int b = 0;
private boolean flag = false;
// 线程1
public void writer() {
a = 1; // 1
b = 2; // 2
flag = true; // 3
}
// 线程2
public void reader() {
if (flag) {
int r1 = a; // 可能看到a=0,b=2(重排序)
int r2 = b;
}
}
}
指令重排序的原因:
- 编译器优化
- CPU指令级并行
- 内存系统重排序
解决方案:
- 使用volatile禁止重排序
- 使用synchronized保证有序性
- 遵循happens-before规则
2.2 内存模型基础
JMM(Java Memory Model)概述
**Java内存模型(JMM)**定义了Java程序中各种变量(实例变量、静态变量等)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量的底层细节。
JMM的目标:
- 屏蔽各种硬件和操作系统的内存访问差异
- 保证Java程序在各种平台下都能达到一致的内存访问效果
- 为多线程编程提供内存可见性保证
主内存与工作内存
主内存(Main Memory):
- 所有共享变量都存储在主内存中
- 主内存是共享的,所有线程都可以访问
工作内存(Working Memory):
- 每个线程都有自己的工作内存
- 工作内存保存了该线程使用到的变量的主内存副本
- 线程对变量的所有操作都必须在工作内存中进行
- 不同线程之间无法直接访问对方的工作内存
内存交互流程:
主内存
↓ read
工作内存(线程1) ←→ 工作内存(线程2)
↓ assign ↓ assign
↓ write ↓ write
↓ store ↓ store
主内存
8种内存操作:
lock(锁定):作用于主内存,把变量标识为线程独占unlock(解锁):作用于主内存,释放锁定状态的变量read(读取):作用于主内存,把变量值从主内存传输到线程工作内存load(载入):作用于工作内存,把read得到的值放入工作内存的变量副本use(使用):作用于工作内存,把工作内存变量值传递给执行引擎assign(赋值):作用于工作内存,把执行引擎接收的值赋给工作内存变量store(存储):作用于工作内存,把工作内存变量值传送到主内存write(写入):作用于主内存,把store传送来的值放入主内存变量
内存可见性
内存可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。
public class MemoryVisibility {
// 没有volatile,可能不可见
private boolean flag = false;
private int count = 0;
public void writer() {
count = 1; // 步骤1
flag = true; // 步骤2
}
public void reader() {
if (flag) { // 步骤3
int r = count; // 步骤4,可能读到0
}
}
}
可见性问题产生的原因:
- CPU缓存:每个CPU核心有自己的缓存,变量可能只更新在缓存中
- 指令重排序:编译器和CPU可能重排序指令
- 寄存器优化:变量可能被优化到寄存器中
保证可见性的方法:
volatile关键字synchronized关键字final关键字(初始化后可见)Lock接口
happens-before规则
happens-before是JMM的核心概念,用于描述两个操作之间的可见性关系。
规则1:程序顺序规则
int a = 1; // 操作1
int b = 2; // 操作2
// 操作1 happens-before 操作2
规则2:volatile规则
volatile int x = 0;
// 线程1
x = 1; // 写操作
// 线程2
int r = x; // 读操作,能看到x=1
规则3:传递性规则
// 如果 A happens-before B,B happens-before C
// 那么 A happens-before C
规则4:监视器锁规则
synchronized (lock) {
// 解锁 happens-before 后续的加锁
}
规则5:start()规则
Thread t = new Thread(() -> {
// 线程t中的操作
});
t.start(); // start() happens-before 线程t中的任何操作
规则6:join()规则
Thread t = new Thread(() -> {
// 线程t中的操作
});
t.start();
t.join(); // 线程t中的所有操作 happens-before join()返回
规则7:线程中断规则
thread.interrupt(); // happens-before 检测到中断
规则8:对象终结规则
// 对象的构造函数 happens-before finalize()方法
as-if-serial语义
as-if-serial语义是指:不管怎么重排序,单线程程序的执行结果不能被改变。
int a = 1; // 1
int b = 2; // 2
int c = a + b; // 3,依赖a和b
// 可以重排序1和2,但不能重排序3到1或2之前
单线程 vs 多线程:
- 单线程:as-if-serial保证结果正确
- 多线程:需要happens-before保证可见性
2.3 线程安全的实现方式
不可变对象
**不可变对象(Immutable Object)**是指对象创建后状态不能被修改。
// 不可变类示例
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
// 没有setter方法,状态不可变
}
实现不可变对象的原则:
- 所有字段都是
final的 - 类声明为
final,防止被继承 - 不提供修改状态的方法
- 如果字段是引用类型,确保引用的对象也是不可变的
Java中的不可变类:
StringInteger、Long等包装类BigInteger、BigDecimal
线程封闭
**线程封闭(Thread Confinement)**是指将对象限制在单个线程中,避免共享。
方式1:栈封闭
public void method() {
int localVar = 0; // 局部变量,线程安全
localVar++;
}
方式2:ThreadLocal
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal =
ThreadLocal.withInitial(() -> 0);
public void increment() {
threadLocal.set(threadLocal.get() + 1);
}
public int get() {
return threadLocal.get();
}
}
同步机制
方式1:synchronized
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
方式2:Lock
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
方式3:原子类
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
无锁编程
**无锁编程(Lock-Free Programming)**使用CAS操作实现线程安全。
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int current;
int next;
do {
current = count.get();
next = current + 1;
} while (!count.compareAndSet(current, next));
}
}
无锁编程的优势:
- 避免锁竞争
- 避免死锁
- 性能更好(在低竞争情况下)
无锁编程的挑战:
- ABA问题
- 自旋开销
- 实现复杂
第三章 synchronized关键字
3.1 synchronized基础
synchronized的三种用法
理解要点:
- synchronized是Java中最基本的同步机制
- 可以锁住代码块,保证同一时刻只有一个线程能执行
- 就像给代码段加上"门锁",一次只允许一个人进入
1. 修饰实例方法
用法: 在方法声明前加上synchronized关键字
public class Counter {
private int count = 0;
// 锁的是当前实例对象(this)
public synchronized void increment() {
count++;
}
// 等价于:
public void increment2() {
synchronized(this) { // 等价写法
count++;
}
}
}
特点说明:
- 锁对象:当前实例对象(this)
- 作用范围:整个方法
- 互斥关系:
- ✅ 同一个实例的多个线程会互斥
- ❌ 不同实例之间不互斥(各自独立)
示例理解:
Counter c1 = new Counter();
Counter c2 = new Counter();
// 线程1和线程2会互斥(同一个实例)
new Thread(() -> c1.increment()).start();
new Thread(() -> c1.increment()).start();
// 线程3不会与线程1、2互斥(不同实例)
new Thread(() -> c2.increment()).start();
2. 修饰静态方法
用法: 在静态方法前加上synchronized关键字
public class Counter {
private static int count = 0;
// 锁的是类对象(Counter.class)
public static synchronized void increment() {
count++;
}
// 等价于:
public static void increment2() {
synchronized(Counter.class) { // 等价写法
count++;
}
}
}
特点说明:
- 锁对象:类对象(Class对象),如
Counter.class - 作用范围:整个静态方法
- 互斥关系:
- ✅ 所有实例共享同一把锁(因为Class对象只有一个)
- ✅ 不同实例的线程也会互斥
- ❌ 静态方法和实例方法的锁不同,不会互斥
示例理解:
Counter c1 = new Counter();
Counter c2 = new Counter();
// 线程1和线程2会互斥(静态方法,共享类锁)
new Thread(() -> Counter.increment()).start();
new Thread(() -> Counter.increment()).start();
// 即使不同实例,也会互斥
new Thread(() -> c1.increment()).start();
new Thread(() -> c2.increment()).start();
// 这两个线程会互斥,因为它们都使用Counter.class作为锁
3. 修饰代码块
用法: 在代码块前加上synchronized(对象)
public class Counter {
private int count = 0;
private final Object lock = new Object(); // 专用锁对象
// 使用指定的锁对象
public void increment() {
synchronized(lock) {
count++;
}
}
// 使用this作为锁(等价于synchronized方法)
public void increment2() {
synchronized(this) {
count++;
}
}
// 使用类对象作为锁(等价于synchronized静态方法)
public static void increment3() {
synchronized(Counter.class) {
// 静态代码块
}
}
}
特点说明:
- 灵活性高:可以指定任意对象作为锁
- 粒度更细:只锁住必要的代码,不锁整个方法
- 性能更好:减少锁的持有时间
为什么推荐使用代码块?
- 可以只锁住必要的代码,而不是整个方法
- 提高并发性能
- 更灵活,可以使用不同的锁对象
锁的对象(重要概念)
核心原则:只有使用同一个对象作为锁,才会互斥
理解要点:
- synchronized锁的是对象,不是代码
- 多个线程使用同一个对象作为锁 → 互斥
- 多个线程使用不同对象作为锁 → 不互斥
private Object lock1 = new Object(); // 锁1
private Object lock2 = new Object(); // 锁2
// 使用lock1作为锁
public void method1() {
synchronized(lock1) {
// 操作1
}
}
// 使用lock2作为锁(与method1不互斥)
public void method2() {
synchronized(lock2) {
// 操作2
}
}
// 使用lock1作为锁(与method1互斥)
public void method3() {
synchronized(lock1) {
// 操作3,与method1互斥
}
}
互斥关系总结:
| 方法 | 使用的锁 | method1 | method2 | method3 |
|---|---|---|---|---|
| method1 | lock1 | ✅ 互斥 | ❌ 不互斥 | ✅ 互斥 |
| method2 | lock2 | ❌ 不互斥 | ✅ 互斥 | ❌ 不互斥 |
| method3 | lock1 | ✅ 互斥 | ❌ 不互斥 | ✅ 互斥 |
注意事项:
- ⚠️ 锁对象不能是null,否则会抛出
NullPointerException - ✅ 推荐使用
final修饰锁对象,防止被重新赋值 - ✅ 使用专门的锁对象(如
private final Object lock),而不是this或业务对象
锁的粒度
什么是锁的粒度?
- 粒度:锁住的范围大小
- 粗粒度:锁住的范围大(如整个方法)
- 细粒度:锁住的范围小(如几行代码)
原则:尽量使用细粒度锁,提高并发性能
粗粒度锁(不推荐)
问题: 锁住了不需要锁的代码,导致不必要的互斥
// ❌ 粗粒度锁:锁住整个方法
private int count1 = 0;
private int count2 = 0;
public synchronized void increment1() {
count1++;
// 其他不相关的操作...
// 整个方法都被锁住,影响性能
}
public synchronized void increment2() {
count2++;
// 与increment1互斥,但实际上没必要
// 因为count1和count2是不同的变量
}
问题分析:
- count1和count2是不同的变量,互不干扰
- 但使用synchronized方法,导致它们互斥
- 降低了并发性能
细粒度锁(推荐)
优点: 只锁住必要的代码,提高并发性
// ✅ 细粒度锁:只锁住必要的代码
private int count1 = 0;
private int count2 = 0;
private final Object lock1 = new Object(); // count1的锁
private final Object lock2 = new Object(); // count2的锁
public void increment1() {
synchronized(lock1) { // 只锁count1相关的操作
count1++;
}
// 其他操作不受锁影响,可以并发执行
}
public void increment2() {
synchronized(lock2) { // 只锁count2相关的操作
count2++;
}
// 与increment1不互斥,可以同时执行
}
性能对比:
- 粗粒度锁:两个线程操作count1和count2时,需要串行执行
- 细粒度锁:两个线程操作count1和count2时,可以并行执行
- 性能提升:细粒度锁明显更好
最佳实践:
- ✅ 使用不同的锁对象保护不同的资源
- ✅ 只锁住必要的代码段
- ✅ 尽量减少锁的持有时间
3.2 synchronized原理
对象头结构
Java对象在内存中的布局:
每个Java对象在内存中都有对象头,对象头中包含了锁的信息。
Java对象内存布局:
┌─────────────────────────────────────┐
│ 对象头(Object Header) │
│ ├── Mark Word(8字节) │ ← 锁信息存储在这里
│ ├── Class Pointer(4/8字节) │ ← 指向类信息
│ └── Array Length(4字节,仅数组) │
├─────────────────────────────────────┤
│ 实例数据(Instance Data) │ ← 对象的字段值
├─────────────────────────────────────┤
│ 对齐填充(Padding) │ ← 内存对齐
└─────────────────────────────────────┘
理解要点:
- Mark Word:最重要的部分,存储锁的状态信息
- Class Pointer:指向类的元数据信息
- JVM通过修改Mark Word来实现锁机制
Mark Word详解
Mark Word是什么?
Mark Word是对象头的一部分,用于存储对象自身的运行时数据,包括:
- 对象的哈希码(hashCode)
- 对象的分代年龄(用于GC)
- 锁的标志位(最重要的)
Mark Word在不同锁状态下存储的内容:
64位JVM的Mark Word结构(简化理解):
Mark Word (64位)
├── 锁状态(2位):标识当前锁的类型
└── 其他数据(62位):根据锁状态不同,存储不同内容
锁状态分类:
┌──────────┬──────────────────────────────────────┐
│ 锁状态 │ Mark Word内容 │
├──────────┼──────────────────────────────────────┤
│ 无锁 │ 对象的hashCode + 分代年龄 + 状态位01 │
│ 偏向锁 │ 线程ID + Epoch + 分代年龄 + 状态位01 │
│ 轻量级锁 │ 指向栈中锁记录的指针 + 状态位00 │
│ 重量级锁 │ 指向monitor对象的指针 + 状态位10 │
│ GC标记 │ 空 + 状态位11 │
└──────────┴──────────────────────────────────────┘
简单理解:
- 无锁:正常对象,没有线程竞争
- 偏向锁:只有一个线程使用,记录线程ID
- 轻量级锁:有竞争但不激烈,使用CAS和自旋
- 重量级锁:竞争激烈,使用操作系统级别的锁
锁的升级过程
锁升级的目的: 根据竞争情况动态调整锁策略,在保证线程安全的前提下,尽可能提高性能。
升级路径:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
(单向升级,不能降级)
无锁状态
特点: 对象刚创建时,没有任何线程使用,处于无锁状态
Object obj = new Object();
// 此时obj的Mark Word处于无锁状态
// 锁标志位:01,没有偏向位
使用场景:
- 对象刚创建
- 没有线程访问同步代码块
- 正常的对象状态
偏向锁(Biased Locking)
设计目的: 优化单线程重复获取锁的场景
适用场景:
- 大多数情况下,只有一个线程使用锁
- 同一个线程多次获取同一个锁
- 没有真正的竞争
工作原理(简单理解):
第一次获取锁:
1. 线程1第一次进入synchronized块
2. 检查Mark Word:是否为可偏向状态?
3. 是:在Mark Word中记录线程1的ID
4. 将锁状态设置为偏向锁
5. 之后线程1再进入时,直接检查线程ID,相同就直接执行
再次获取锁(同一线程):
1. 线程1再次进入synchronized块
2. 检查Mark Word中的线程ID:是否是自己?
3. 是:直接执行,无需任何同步操作(很快!)
4. 就像"免检通道",无需排队
代码示例(简化理解):
public class Counter {
public synchronized void increment() {
// 第一次:升级为偏向锁,记录线程ID
// 之后同一线程:直接执行,几乎无开销
}
}
// 场景:单线程场景
Counter c = new Counter();
// 线程A多次调用,都很快(偏向锁优化)
for (int i = 0; i < 1000; i++) {
c.increment(); // 第二次开始就很快了
}
偏向锁的优势:
- ✅ 性能极好:同一线程再次获取锁几乎无开销
- ✅ 适合单线程场景:大多数情况下就是单线程使用
- ✅ 减少同步开销:避免CAS操作
偏向锁的获取流程:
1. 检查Mark Word的锁标志位(是否为01,可偏向)
├─ 是 → 继续
└─ 否 → 已有其他锁状态,跳过偏向锁
2. 检查线程ID是否指向当前线程
├─ 是 → 直接进入同步代码块(最快路径)
└─ 否 → 尝试CAS替换线程ID
├─ 成功 → 获得偏向锁
└─ 失败 → 撤销偏向锁,升级为轻量级锁
轻量级锁(Lightweight Locking)
设计目的: 当有多个线程竞争,但竞争不激烈时,使用CAS自旋代替阻塞
适用场景:
- 有多个线程竞争锁
- 但竞争不激烈(大部分CAS能成功)
- 等待时间短
工作原理(简化理解):
获取锁的过程:
1. 在栈中创建锁记录(Lock Record)
2. 复制对象头的Mark Word到锁记录(备份)
3. CAS尝试将对象头的Mark Word替换为锁记录的指针
├─ 成功 → 获得轻量级锁(很快,使用CAS)
└─ 失败 → 有其他线程在竞争,自旋重试
├─ 自旋成功 → 获得锁
└─ 自旋失败(自旋次数过多)→ 升级为重量级锁
为什么叫"轻量级"?
- 不需要操作系统介入(重量级锁需要)
- 使用CAS自旋,线程不阻塞
- 开销比重量级锁小
代码示例:
public class Counter {
public synchronized void increment() {
// 多线程竞争,但竞争不激烈
// 使用轻量级锁:CAS + 自旋
count++;
}
}
// 场景:多个线程,但竞争不激烈
Counter c = new Counter();
// 多个线程同时调用,但大多数情况下CAS能成功
// 只有少数情况需要自旋重试
轻量级锁的特点:
- ✅ 使用CAS:无锁编程思想,性能好
- ✅ 自旋重试:失败后自旋几次,避免立即阻塞
- ⚠️ CPU消耗:自旋会占用CPU,不适合高竞争场景
- ⚠️ 可能升级:自旋失败后升级为重量级锁
重量级锁(Heavyweight Locking)
设计目的: 当竞争激烈时,使用操作系统级别的互斥量,让线程阻塞等待
适用场景:
- 锁竞争激烈(很多线程同时竞争)
- 轻量级锁自旋失败(自旋次数超过阈值)
- 等待时间可能很长
工作原理(简化理解):
升级过程:
1. 轻量级锁自旋失败(多次CAS失败)
2. 锁升级为重量级锁
3. 对象头的Mark Word指向monitor对象(管程)
4. 竞争失败的线程进入阻塞队列
5. 由操作系统进行线程调度和唤醒
为什么叫"重量级"?
- 需要操作系统介入(操作系统级别的mutex)
- 线程会阻塞,需要上下文切换
- 开销比轻量级锁大
代码示例:
public class Counter {
public synchronized void increment() {
// 很多线程同时竞争
// 使用重量级锁:线程阻塞等待
count++;
}
}
// 场景:高竞争
Counter c = new Counter();
// 100个线程同时竞争,轻量级锁自旋失败
// 升级为重量级锁,大部分线程阻塞等待
重量级锁的特点:
- ✅ 适合高竞争:竞争激烈时性能稳定
- ✅ 节省CPU:阻塞的线程不占用CPU
- ❌ 开销大:需要操作系统介入,上下文切换开销
- ❌ 响应慢:线程阻塞后需要等待被唤醒
锁升级的条件和时机
升级路径(单向,不能降级):
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
(一旦升级,不能降级)
升级条件详解:
1. 无锁 → 偏向锁
触发条件:
- 对象被第一个线程访问同步代码块
- JVM启用偏向锁(JDK 15+默认禁用,但了解原理很重要)
时机:
Object obj = new Object(); // 无锁状态
// 第一次访问
synchronized(obj) {
// 升级为偏向锁,记录当前线程ID
}
2. 偏向锁 → 轻量级锁
触发条件:
- 有其他线程尝试获取偏向锁(发现线程ID不是自己)
- 偏向锁撤销,升级为轻量级锁
时机:
// 线程1获得偏向锁
synchronized(obj) {
// 偏向锁状态
}
// 线程2尝试获取锁(发现线程ID不是自己)
// 触发偏向锁撤销,升级为轻量级锁
synchronized(obj) {
// 轻量级锁状态,使用CAS竞争
}
3. 轻量级锁 → 重量级锁
触发条件:
- 自旋失败(CAS失败次数超过阈值,如10次)
- 等待的线程数超过1个
- 自旋时间过长
时机:
// 多个线程竞争,CAS频繁失败
// 自旋多次后仍然失败
synchronized(obj) {
// 升级为重量级锁
// 失败的线程进入阻塞队列
}
升级决策流程(简化理解):
线程尝试获取锁
↓
是否有竞争?
├─ 无 → 偏向锁(记录线程ID)
└─ 有 → 轻量级锁(CAS + 自旋)
↓
自旋是否成功?
├─ 成功 → 保持轻量级锁
└─ 失败(超过阈值)→ 重量级锁(阻塞等待)
查看锁状态(了解即可):
可以使用JOL(Java Object Layout)工具查看对象头的锁状态:
// 需要添加依赖:org.openjdk.jol:jol-core
import org.openjdk.jol.info.ClassLayout;
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 可以看到Mark Word的详细信息,包括锁状态
3.3 synchronized的优化
JVM会对synchronized进行多种优化,提高性能。
锁消除(Lock Elimination)
原理: JVM在JIT编译时,分析代码后发现某个锁没有必要,就自动消除它。
为什么会消除?
- 如果对象不会"逃逸"出方法(其他线程访问不到)
- 就没有多线程竞争,锁就没有必要
示例:
// StringBuffer是线程安全的(内部有synchronized)
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer(); // 局部变量,不会逃逸
sb.append(s1); // 这些操作虽然有锁,但JVM会消除
sb.append(s2);
sb.append(s3);
return sb.toString();
}
// JVM分析后发现:sb是局部变量,其他线程访问不到
// 优化:消除StringBuffer内部的锁,直接操作(更快)
触发条件:
- ✅ 对象是局部变量,不会逃逸出方法
- ✅ 没有其他线程能访问到这个对象
- ✅ JVM在JIT编译时检测到
效果: 消除不必要的锁开销,提高性能
锁粗化(Lock Coarsening)
原理: 将多个连续的加锁、解锁操作合并成一个更大的锁。
为什么需要粗化?
- 频繁加锁解锁有开销
- 如果连续的同步块使用同一个锁,可以合并
示例:
// ❌ 原始代码:多个连续的同步块
public void method() {
synchronized(this) {
count1++; // 操作1
}
synchronized(this) {
count2++; // 操作2
}
synchronized(this) {
count3++; // 操作3
}
}
// ✅ JVM优化后:合并为一个同步块
public void method() {
synchronized(this) {
count1++; // 操作1
count2++; // 操作2
count3++; // 操作3
}
}
适用场景:
- ✅ 循环中的同步操作
- ✅ 连续的同步块(使用同一个锁)
- ✅ 锁的粒度可以适当增大而不影响性能
效果: 减少加锁解锁的次数,提高性能
自适应自旋(Adaptive Spinning)
原理: 自旋次数不再固定,而是根据历史情况动态调整。
为什么需要自适应?
- 固定自旋次数可能浪费CPU(太多次)
- 也可能错过机会(太少了)
自适应策略:
如果之前自旋成功过:
→ 增加自旋次数(可能很快就能获得锁)
如果之前自旋很少成功:
→ 减少自旋次数(减少CPU浪费)
→ 甚至直接阻塞(不浪费时间自旋)
简单理解:
- JVM会"学习"这个锁的特性
- 根据历史成功率调整策略
- 智能优化,提高效率
效果: 平衡CPU消耗和性能,智能优化
偏向锁的撤销
什么时候撤销偏向锁?
撤销场景:
- 有其他线程竞争:发现线程ID不是当前线程
- 调用hashCode():偏向锁的Mark Word没有空间存储hashCode
- 调用wait()/notify():这些方法需要重量级锁(monitor)
撤销过程(简单理解):
1. 暂停拥有偏向锁的线程(安全点)
2. 检查线程状态:
├─ 还在执行同步代码块 → 升级为轻量级锁
└─ 已经离开同步代码块 → 恢复到无锁状态
3. 唤醒暂停的线程
为什么需要安全点?
- 撤销时需要修改Mark Word
- 必须在线程安全点进行(线程暂停状态)
- 确保操作的原子性
3.4 synchronized的局限性
虽然synchronized很强大,但也有一些限制。
不可中断
问题: 线程在等待synchronized锁时,无法响应中断
示例:
private final Object lock = new Object();
// 线程1:持有锁,执行很长时间
public void method1() {
synchronized(lock) {
try {
Thread.sleep(10000); // 持有锁10秒
} catch (InterruptedException e) {
// 即使被中断,也会继续持有锁
}
}
}
// 线程2:等待获取锁
public void method2() {
// ⚠️ 问题:无法中断这个等待
// 即使调用thread.interrupt(),线程2也不会响应
synchronized(lock) {
// 必须等待method1释放锁
}
}
问题分析:
- 线程2在等待锁时,如果调用
thread2.interrupt(),线程2不会响应 - 必须等到线程1释放锁,线程2才能继续
- 可能导致线程一直等待
解决方案: 使用ReentrantLock.lockInterruptibly(),可以响应中断
非公平锁
问题: synchronized是非公平的,可能导致线程饥饿
什么是非公平?
- 新来的线程可能"插队"
- 比等待队列中的线程先获得锁
示例:
private final Object lock = new Object();
// 线程A:等待锁(在队列中)
public void methodA() {
synchronized(lock) {
// 线程A在队列中等待了很久
}
}
// 线程B:新来的线程
public void methodB() {
// ⚠️ 线程B可能比线程A先获得锁(非公平)
synchronized(lock) {
// 新线程可能插队成功
}
}
问题分析:
- 等待时间长的线程可能一直获取不到锁
- 新来的线程可能不断插队
- 导致某些线程"饥饿"(一直等待)
解决方案: 使用ReentrantLock(true)创建公平锁
性能问题
历史演进:
JDK 1.6之前:
- synchronized是重量级锁
- 每次加锁都需要操作系统参与
- 性能较差
JDK 1.6之后:
- ✅ 引入了锁升级机制(偏向锁 → 轻量级锁 → 重量级锁)
- ✅ 性能大幅提升
- ✅ 在低竞争情况下,性能接近无锁
性能对比:
| 场景 | synchronized | ReentrantLock |
|---|---|---|
| 低竞争 | ✅ 性能很好(锁升级优化) | ✅ 性能也很好 |
| 高竞争 | ⚠️ 可能升级为重量级锁 | ✅ 性能可能更好 |
| 可中断 | ❌ 不支持 | ✅ 支持 |
| 公平锁 | ❌ 非公平 | ✅ 可选公平/非公平 |
建议:
- 大多数情况下,synchronized性能已经很好
- 需要可中断或公平锁时,使用ReentrantLock
- 低竞争场景:两者性能接近
第四章 volatile关键字
4.1 volatile的作用
volatile是什么?
- volatile是一个关键字,用于修饰变量
- 保证变量的可见性和有序性
- 但不保证原子性
保证可见性
什么是可见性问题?
在多核CPU环境下,每个CPU都有自己的缓存。线程可能在自己的CPU缓存中保存变量的副本,导致一个线程的修改,其他线程看不到。
可见性问题示例:
// ❌ 问题代码:没有volatile
private boolean flag = false;
public void start() {
new Thread(() -> {
while (!flag) {
// ⚠️ 可能永远循环
// 线程在自己的CPU缓存中读取flag=false
// 看不到主线程修改flag=true
}
System.out.println("线程结束");
}).start();
}
public void stop() {
flag = true;
// ⚠️ 修改可能只更新在CPU缓存中
// 没有刷新到主内存,其他线程看不到
}
问题原因:
- CPU缓存:每个CPU有自己的缓存,变量可能只存在缓存中
- 缓存未同步:修改没有刷新到主内存
- 其他线程看不到:从自己的缓存读取,还是旧值
使用volatile解决:
// ✅ 解决方案:使用volatile
private volatile boolean flag = false;
public void start() {
new Thread(() -> {
while (!flag) {
// ✅ 能及时看到flag的变化
// volatile保证从主内存读取最新值
}
System.out.println("线程结束");
}).start();
}
public void stop() {
flag = true;
// ✅ 修改立即刷新到主内存
// 其他线程能立即看到
}
volatile的可见性保证(简单理解):
写volatile变量:
1. 线程修改volatile变量
2. 立即刷新到主内存(不是只写缓存)
3. 使其他CPU的缓存失效
读volatile变量:
1. 线程读取volatile变量
2. 从主内存读取(不从缓存读)
3. 保证读到最新值
生活化理解:
- 没有volatile:就像每个人有自己的笔记本,修改了但别人看不到
- 有volatile:就像写在公告板上,所有人都能看到最新内容
禁止指令重排序
什么是指令重排序?
为了优化性能,编译器和CPU可能会重新排列指令的执行顺序。在单线程下没问题,但在多线程下可能导致问题。
重排序问题示例:
// ❌ 问题代码:可能重排序
private int a = 0;
private int b = 0;
private boolean flag = false; // 没有volatile
// 线程1
public void writer() {
a = 1; // 指令1
b = 2; // 指令2
flag = true; // 指令3
// ⚠️ 可能被重排序为:3 -> 1 -> 2
// CPU或编译器可能优化执行顺序
}
// 线程2
public void reader() {
if (flag) {
int r1 = a; // ⚠️ 可能看到a=0(还没执行a=1)
int r2 = b; // 可能看到b=2
// 因为指令重排序,看到不一致的状态
}
}
问题分析:
- 指令1和2可能在指令3之后执行(重排序)
- 线程2看到flag=true时,a可能还是0
- 导致数据不一致
使用volatile解决:
// ✅ 解决方案:使用volatile禁止重排序
private int a = 0;
private int b = 0;
private volatile boolean flag = false; // volatile禁止重排序
// 线程1
public void writer() {
a = 1; // 1
b = 2; // 2
flag = true; // 3
// ✅ volatile写:前面的操作不能重排序到后面
// 保证a=1和b=2在flag=true之前完成
}
// 线程2
public void reader() {
if (flag) {
// ✅ volatile读:后面的操作不能重排序到前面
int r1 = a; // 保证看到a=1(因为volatile保证有序性)
int r2 = b; // 保证看到b=2
}
}
volatile的内存屏障(简化理解):
volatile通过插入内存屏障来禁止重排序:
写volatile变量:
普通写1
普通写2
─────── StoreStore屏障 ─────── ← 禁止上面的普通写和volatile写重排序
volatile写
─────── StoreLoad屏障 ─────── ← 禁止volatile写和下面的操作重排序
读volatile变量:
volatile读
─────── LoadLoad屏障 ─────── ← 禁止volatile读和下面的读重排序
─────── LoadStore屏障 ─────── ← 禁止volatile读和下面的写重排序
普通读
简单理解:
- 内存屏障就像"栏杆",阻止指令跨越
- volatile写前的操作不能移到写之后
- volatile读后的操作不能移到读之前
- 保证有序性
不保证原子性
什么是原子性?
- 原子性:操作要么全部执行,要么都不执行,不会被打断
- volatile只保证可见性和有序性,不保证原子性
原子性问题示例:
// ❌ 错误:volatile不能保证原子性
private volatile int count = 0;
public void increment() {
count++; // ⚠️ 不是原子操作
}
// 测试
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
increment();
}
});
t1.start();
t2.start();
// 结果:期望20000,实际可能小于20000 ❌
为什么count++不是原子的?
count++实际上包含三个步骤:
// count++的分解步骤
1. 读取count的值 (read)
2. 将count加1 (add)
3. 将新值写回count (write)
// 问题:这三个步骤之间可能被其他线程打断
线程1:read(count=100) → add(101) → [被线程2打断]
线程2:read(count=100) → add(101) → write(101)
线程1:write(101) // 两个线程都加了1,但结果只加了1次
volatile为什么不能保证原子性?
- volatile只能保证单个读写操作的可见性
- 但
count++是多个操作(读-改-写) - volatile无法保证这三个操作作为一个整体执行
解决方案:
// ✅ 方案1:使用synchronized
private int count = 0;
public synchronized void increment() {
count++; // 整个方法原子执行
}
// ✅ 方案2:使用原子类(推荐)
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作,内部使用CAS
}
总结:
- ✅ volatile保证:可见性、有序性
- ❌ volatile不保证:原子性(需要synchronized或原子类)
4.2 volatile的实现原理
volatile如何实现可见性和有序性?
- 通过内存屏障(Memory Barrier)实现
- 通过Lock前缀指令(CPU级别)实现
- 通过MESI缓存一致性协议(硬件级别)实现
内存屏障(Memory Barrier)
什么是内存屏障?
内存屏障是一类CPU指令,用于:
- ✅ 阻止重排序:确保屏障两侧的指令不会重排序
- ✅ 保证可见性:强制刷新缓存,使修改对其他CPU可见
简单理解:
- 就像道路上的"栏杆",阻止车辆(指令)跨越
- 确保指令按照预期顺序执行
- 强制数据同步到主内存
JMM定义的四种内存屏障:
1. LoadLoad屏障
Load1; // 加载操作1
LoadLoad屏障; // 栏杆
Load2; // 加载操作2
作用:确保Load1在Load2之前执行
2. StoreStore屏障
Store1; // 存储操作1
StoreStore屏障; // 栏杆
Store2; // 存储操作2
作用:确保Store1的数据刷新到主内存,再执行Store2
3. LoadStore屏障
Load1; // 加载操作1
LoadStore屏障; // 栏杆
Store2; // 存储操作2
作用:确保Load1在Store2之前执行
4. StoreLoad屏障
Store1; // 存储操作1
StoreLoad屏障; // 栏杆(最重,开销最大)
Load2; // 加载操作2
作用:确保Store1的数据刷新到主内存,再执行Load2
volatile的内存屏障插入策略(简化理解):
volatile int x = 0;
int a = 1;
// volatile写操作
a = 1; // 普通写
─────── StoreStore屏障 ─────── ← 禁止普通写和volatile写重排序
x = 1; // volatile写
─────── StoreLoad屏障 ─────── ← 禁止volatile写和后面的操作重排序
// volatile读操作
int r1 = x; // volatile读
─────── LoadLoad屏障 ─────── ← 禁止volatile读和后面的读重排序
─────── LoadStore屏障 ─────── ← 禁止volatile读和后面的写重排序
int r2 = a; // 普通读
关键点:
- volatile写前插入StoreStore屏障
- volatile写后插入StoreLoad屏障
- volatile读后插入LoadLoad和LoadStore屏障
- 通过这些屏障保证可见性和有序性
Lock前缀指令
x86架构下的实现:
volatile写操作在x86架构下会被编译为带有lock前缀的指令。
汇编代码示例(了解即可):
; volatile写操作
mov %eax,0x10(%esi) ; 将值写入内存
lock addl $0x0,(%esp) ; lock前缀,锁定缓存行
lock前缀的作用:
-
锁定总线或缓存行
- 在多核CPU中,确保只有一个CPU能执行这个指令
- 保证操作的原子性
-
刷新缓存
- 将缓存中的数据写回主内存
- 使其他CPU的缓存失效
- 保证可见性
简单理解:
- lock前缀就像给操作加了一把"锁"
- 确保操作是原子的、可见的
- CPU硬件层面的保证
MESI缓存一致性协议
什么是MESI?
MESI是多核CPU的缓存一致性协议,用于保证多核CPU之间缓存的一致性。
MESI的四种状态:
| 状态 | 全称 | 含义 |
|---|---|---|
| M | Modified | 缓存行被修改,与主内存不一致 |
| E | Exclusive | 缓存行独占,与主内存一致 |
| S | Shared | 缓存行共享,与主内存一致 |
| I | Invalid | 缓存行无效,需要从主内存加载 |
volatile变量的缓存一致性(简化理解):
场景:CPU1写volatile变量
1. CPU1修改volatile变量
→ 缓存行状态变为M(Modified)
2. 通过总线发送消息
→ 通知其他CPU:这个缓存行已失效
3. 其他CPU收到消息
→ 将对应缓存行状态改为I(Invalid)
4. 其他CPU读取时
→ 发现缓存无效,从主内存重新加载
→ 保证读到最新值
为什么需要MESI?
- 每个CPU有自己的缓存
- 需要保证所有CPU看到的数据一致
- MESI协议自动处理缓存一致性
4.3 volatile的使用场景
状态标志
最常用的场景: 使用volatile作为线程间的状态标志
示例:
// ✅ 推荐:使用volatile作为状态标志
private volatile boolean shutdown = false;
public void shutdown() {
shutdown = true; // 其他线程能立即看到
}
public void doWork() {
while (!shutdown) {
// 执行任务
// 能及时响应shutdown的变化
}
}
为什么适合用volatile?
- ✅ 只需要可见性(线程间通信)
- ✅ 不需要原子性(只是boolean标志)
- ✅ 简单高效(比synchronized轻量)
适用场景:
- ✅ 线程启动/停止标志
- ✅ 配置开关
- ✅ 状态切换标志
双重检查锁定(DCL)
什么是DCL?
双重检查锁定是一种单例模式的实现方式,通过两次检查来减少锁的使用。
错误的单例模式:
// ❌ 错误:可能有问题
public class Singleton {
private static Singleton instance; // 没有volatile
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized(Singleton.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton(); // ⚠️ 可能重排序
}
}
}
return instance;
}
}
问题:对象创建可能重排序
new Singleton()包含三个步骤,可能被重排序:
// 正常顺序
1. 分配内存空间
2. 初始化对象(调用构造函数)
3. 将引用赋值给instance
// 可能重排序为(危险!)
1. 分配内存空间
3. 将引用赋值给instance // instance != null,但对象未初始化!
2. 初始化对象
// 问题:线程B可能拿到未完全初始化的对象
使用volatile解决:
// ✅ 正确:使用volatile
public class Singleton {
private static volatile Singleton instance; // volatile禁止重排序
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
// volatile保证:先初始化对象,再赋值
}
}
}
return instance;
}
}
volatile的作用:
- ✅ 禁止重排序:确保对象完全初始化后才赋值
- ✅ 保证可见性:其他线程能看到完整的对象
DCL的工作原理:
第一次检查(无锁):快速路径,大多数情况下直接返回
↓
如果为null,进入同步块
↓
第二次检查(有锁):确保只创建一个实例
↓
如果仍为null,创建实例
优势:
- 第一次检查无锁,性能好
- 只在第一次创建时加锁
- 之后都是无锁访问
单例模式中的应用
枚举方式(推荐):
public enum Singleton {
INSTANCE;
public void doSomething() {
// ...
}
}
静态内部类方式:
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
4.4 volatile vs synchronized
性能对比
volatile的性能:
- ✅ 读操作:性能接近普通变量(只是从主内存读)
- ⚠️ 写操作:需要刷新缓存,性能略差(但比synchronized好)
- ✅ 总体:性能优于synchronized
synchronized的性能:
- JDK 1.6之前:重量级锁,性能较差
- JDK 1.6之后:锁升级优化,性能大幅提升
- 总体:在低竞争情况下,性能接近volatile
性能对比(简化理解):
低竞争场景:
volatile: 很快(几乎无开销)
synchronized: 较快(偏向锁/轻量级锁)
高竞争场景:
volatile: 仍然很快(只是刷新缓存)
synchronized: 可能变慢(升级为重量级锁)
使用场景对比
功能对比表:
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ 保证 | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 有序性 | ✅ 保证 | ✅ 保证 |
| 互斥性 | ❌ 不保证 | ✅ 保证 |
| 性能 | ✅ 较高 | ⚠️ 中等 |
| 使用复杂度 | ✅ 简单 | ⚠️ 中等 |
volatile适用场景:
- ✅ 状态标志:boolean类型的线程间通信
- ✅ 双重检查锁定:单例模式
- ✅ 读多写少:只需要可见性,不需要互斥
- ✅ 独立观察:发布观察结果给其他线程
synchronized适用场景:
- ✅ 需要原子性:多步骤操作需要原子执行
- ✅ 需要互斥:同一时刻只能有一个线程执行
- ✅ 复杂同步:需要更复杂的同步逻辑
选择建议:
只需要可见性?
├─ 是 → 使用volatile ✅
└─ 否 → 需要原子性?
├─ 是 → 使用synchronized或原子类
└─ 否 → 根据具体情况选择
读多写少?
├─ 是 → volatile + CAS ✅
└─ 否 → synchronized
简单标志?
├─ 是 → volatile ✅
└─ 否 → synchronized
实际建议:
- 状态标志:优先使用volatile
- 计数器等:使用原子类(AtomicInteger)
- 复杂同步:使用synchronized
- 读多写少:volatile + CAS
第五章 CAS(Compare-And-Swap)
5.1 CAS原理
CAS操作的定义
**CAS(Compare-And-Swap)**是一种无锁的原子操作,用于实现多线程同步。
什么是CAS?
- CAS是"比较并交换"的意思
- 它是一种乐观锁的实现方式
- 不像悲观锁(如synchronized)先获取锁再操作,CAS先尝试操作,失败了再重试
生活化理解:
- 想象一个储物柜,你想把里面的东西换掉
- CAS就是:先看看里面是不是你期望的东西(比较)
- 如果是,就换成新的(交换)
- 如果不是,说明被别人换过了,重新读取再尝试
CAS操作包含三个操作数:
- 内存位置(V):要更新的变量(就像储物柜的位置)
- 预期值(A):期望的旧值(你期望看到的旧东西)
- 新值(B):要设置的新值(你想放进去的新东西)
CAS操作逻辑(简单理解):
1. 读取当前值 V
2. 比较:V 是否等于预期值 A?
- 如果相等 → 将 V 更新为 B(交换成功)
- 如果不相等 → 不更新(交换失败,可能被别人改过了)
3. 返回操作结果
伪代码(简化版):
public boolean compareAndSwap(int V, int A, int B) {
if (V == A) { // 比较:当前值是否等于预期值?
V = B; // 交换:更新为新值
return true; // 成功
}
return false; // 失败,返回当前值
}
实际返回值:
- 有些CAS实现返回boolean(成功/失败)
- 有些返回旧值(让你知道当前的实际值)
CAS的原子性保证
为什么CAS是原子的?
CAS之所以是原子操作,是因为CPU直接提供了原子性的CAS指令。这不是Java语言层面的特性,而是硬件层面的支持。
关键点:
- CPU指令级别:CAS是CPU的一条指令,一条指令的执行是不可分割的
- 不会被中断:在执行CAS指令期间,不会被其他线程或操作中断
- 硬件保证:这是硬件层面的保证,比软件层面的锁更底层、更高效
原子性的重要性:
// ❌ 非原子操作(不安全,有竞态条件)
if (value == expected) {
value = newValue; // 这两步不是原子的
// 问题:在检查和赋值之间,value可能被其他线程修改
}
// ✅ CAS原子操作(安全)
compareAndSwap(value, expected, newValue);
// 一步完成,不会被中断,原子性保证
为什么普通操作不是原子的?
- 普通操作包含多个步骤(读取、比较、写入)
- 在多线程环境下,这些步骤之间可能被其他线程打断
- 导致数据不一致问题
CPU原语支持
不同CPU架构的CAS实现:
x86/x64架构(Intel/AMD):
; CMPXCHG指令(Compare and Exchange)
CMPXCHG dest, src
; 功能:比较EAX寄存器中的值和dest,如果相等,将src写入dest
; 这是x86架构提供的原子指令
ARM架构(手机/嵌入式设备):
; LDREX/STREX指令对(Load-Exclusive/Store-Exclusive)
LDREX R1, [R0] ; 加载并独占访问
CMP R1, R2 ; 比较
STREXEQ R3, R4, [R0] ; 条件存储(如果独占状态还在)
; 通过独占访问机制实现原子操作
Java中的CAS:
Java通过Unsafe类调用底层CPU指令,对开发者来说是透明的。
// Unsafe类提供CAS方法(底层调用CPU指令)
public final native boolean compareAndSwapInt(
Object o, // 对象
long offset, // 字段偏移量(内存地址)
int expected, // 预期值
int x // 新值
);
// 这个方法最终会调用CPU的CAS指令
简单理解:
- Java代码 → Unsafe类 → JVM → CPU指令
- 最终执行的是CPU提供的原子指令
- 开发者不需要关心底层实现细节
5.2 CAS的实现
Unsafe类
Unsafe类是什么?
Unsafe类是Java提供的一个"后门"类,用于执行一些不安全的底层操作。它的名字就说明了它的特性——unsafe(不安全)。
Unsafe类的作用:
- ✅ 直接操作内存:可以像C语言一样直接读写内存
- ✅ 提供CAS方法:compareAndSwapInt、compareAndSwapLong等
- ✅ 绕过安全检查:可以做一些正常情况下不允许的操作
- ⚠️ 不推荐直接使用:属于
sun.misc包,不是公开API,可能在不同JDK版本中变化
为什么叫Unsafe?
- 因为它绕过了Java的安全检查机制
- 使用不当可能导致JVM崩溃
- 只有系统代码(如JUC包)才应该使用
获取Unsafe实例(仅了解,不要直接使用):
// ⚠️ 注意:这只是演示,生产环境不要这样做
import sun.misc.Unsafe;
import java.lang.reflect.Field;
// 通过反射获取Unsafe实例
Unsafe unsafe = getUnsafe();
// 正常开发中,应该使用AtomicInteger等封装好的类
// 而不是直接使用Unsafe
实际开发建议:
- ❌ 不要直接使用Unsafe类
- ✅ 使用AtomicInteger、AtomicLong等封装好的原子类
- ✅ 这些原子类内部已经使用了Unsafe,提供安全的API
compareAndSwapInt/Long/Object方法
Unsafe提供的CAS方法:
// 针对int类型的CAS
boolean compareAndSwapInt(Object o, long offset, int expected, int x);
// 针对long类型的CAS
boolean compareAndSwapLong(Object o, long offset, long expected, long x);
// 针对对象引用的CAS
boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
参数说明:
Object o:包含要更新字段的对象long offset:字段在对象中的内存偏移量(可以理解为字段的"地址")expected:期望的旧值x:要设置的新值
实际使用(简化示例):
// 实际开发中,不需要自己实现,直接使用AtomicInteger即可
AtomicInteger count = new AtomicInteger(0);
// incrementAndGet内部就是使用CAS实现的
count.incrementAndGet();
// 等价于以下逻辑(简化版):
// do {
// current = count.get();
// next = current + 1;
// } while (!count.compareAndSet(current, next));
方法对比:
- compareAndSwapInt:用于int类型(32位)
- compareAndSwapLong:用于long类型(64位)
- compareAndSwapObject:用于对象引用
CAS的底层实现(了解即可)
x86架构下的实现原理:
JVM的HotSpot虚拟机在x86架构下,CAS最终会编译成CPU指令。
// HotSpot源码(简化版,了解即可)
inline jint Atomic::cmpxchg(...) {
__asm__ volatile (
"lock cmpxchgl %1,(%3)" // 关键:lock前缀 + CMPXCHG指令
...
);
}
关键点解析:
-
lock前缀:
- 锁定CPU总线或缓存行
- 确保只有一个CPU核心能执行这个指令
- 保证原子性
-
cmpxchgl指令:
- x86架构的比较并交换指令
- 一条指令完成比较和交换
- 硬件级别的原子操作
-
内存屏障:
- 保证可见性(其他CPU能看到更新)
- 防止指令重排序
简单理解:
- Java代码 → JVM → CPU指令(lock cmpxchgl)
- 一条CPU指令完成,不会被中断
- 硬件保证原子性,非常高效
5.3 CAS的优缺点
优点:无锁、高性能
无锁的优势:
-
避免线程阻塞和唤醒的开销
- synchronized会让线程进入阻塞状态,需要操作系统唤醒
- CAS失败后只是自旋重试,线程不会阻塞
- 减少了上下文切换的开销
-
避免死锁
- CAS不需要获取锁,不会出现死锁问题
- 非常适合在高并发场景使用
-
适合低竞争场景
- 当多个线程竞争不激烈时,CAS性能非常好
- 大多数情况下CAS都能成功,不需要重试
性能对比代码示例:
// ❌ synchronized方式(有锁,会阻塞)
private int count = 0;
public synchronized void increment() {
count++; // 获取锁,可能阻塞等待
}
// ✅ CAS方式(无锁,自旋重试)
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
// 内部实现:自旋重试,不阻塞
// do {
// current = count.get();
// } while (!count.compareAndSet(current, current + 1));
}
性能对比总结:
| 场景 | CAS | synchronized |
|---|---|---|
| 低竞争 | ✅ 性能很好(大多数成功,很少重试) | ⚠️ 性能稍差(有锁开销) |
| 高竞争 | ⚠️ 性能下降(大量自旋重试,CPU消耗高) | ✅ 性能更好(阻塞等待,节省CPU) |
| 推荐场景 | 读多写少、竞争不激烈 | 竞争激烈、需要互斥访问 |
缺点:ABA问题、自旋开销、只能保证一个变量的原子性
ABA问题
什么是ABA问题?
ABA问题是指:值从A变成B,再变回A,CAS仍然认为值没有被修改过。
生活化理解:
- 你离开时看到桌上有个苹果(A)
- 你回来时桌上还是苹果(A)
- 但实际上这个苹果可能被换过了(原来的被吃了,放了个新的)
- CAS只检查值是否相同,无法发现"被换过"这个事实
问题示例:
时间线:
T1: 线程1读取值 = A
T2: 线程2修改值:A -> B
T3: 线程2修改值:B -> A(又改回来了)
T4: 线程1执行CAS(A, C)
结果:CAS成功!但实际上值已经被修改过了
示例代码(简化版):
AtomicReference<String> ref = new AtomicReference<>("A");
// 线程1:准备将A改为C
Thread t1 = new Thread(() -> {
String old = ref.get(); // 读取"A"
Thread.sleep(1000); // 等待1秒
// 此时值可能已经被线程2改过,但CAS仍然成功
ref.compareAndSet(old, "C"); // 成功!但可能不是期望的结果
});
// 线程2:A -> B -> A
Thread t2 = new Thread(() -> {
ref.compareAndSet("A", "B"); // A -> B
ref.compareAndSet("B", "A"); // B -> A(又改回来了)
});
t1.start();
t2.start();
ABA问题的危害:
- 在栈、链表等数据结构中,可能导致逻辑错误
- 虽然值相同,但对象可能已经被替换过
- 需要额外的版本号或标记来检测
解决方案:
- 使用版本号:每次修改版本号+1(AtomicStampedReference)
- 使用标记位:标记是否被修改过(AtomicMarkableReference)
自旋开销
问题描述:
CAS失败后会不断重试(自旋),在高竞争场景下会浪费CPU。
具体表现:
// CAS自旋过程(伪代码)
do {
current = value; // 读取当前值
next = current + 1;
} while (!compareAndSet(current, next));
// 如果一直失败,会一直循环(自旋),消耗CPU
问题:
- ⚠️ CPU消耗高:自旋会持续占用CPU,不做其他工作
- ⚠️ 高竞争时性能下降:大量线程同时CAS,失败率高,自旋时间长
- ⚠️ 可能导致CPU 100%:所有线程都在自旋,CPU满载但效率低
解决方案:
- 限制自旋次数:超过一定次数后放弃
- 自适应自旋:根据历史成功率动态调整自旋次数
- 自旋失败后阻塞:自旋一段时间后如果还失败,就阻塞等待
性能建议:
- 低竞争场景:使用CAS(自旋开销小)
- 高竞争场景:考虑使用锁(阻塞等待,节省CPU)
只能保证一个变量的原子性
问题描述:
CAS只能原子地更新一个变量。如果需要对多个变量进行原子操作,CAS无法直接保证。
示例:
AtomicInteger count1 = new AtomicInteger(0);
AtomicInteger count2 = new AtomicInteger(0);
// ❌ 这两个操作不是原子的
count1.incrementAndGet(); // 操作1
count2.incrementAndGet(); // 操作2
// 问题:两个操作之间可能被其他线程打断
为什么这是个问题?
- 如果需要保证count1和count2同时更新,CAS无法做到
- 两个独立的CAS操作之间没有原子性保证
- 可能导致数据不一致
解决方案:
- 使用synchronized:将多个操作放在同步块中
- 使用锁:ReentrantLock等
- 合并变量:将多个变量合并为一个对象,CAS更新整个对象
- 使用AtomicReference:将多个值封装在一个对象中
示例:
// ✅ 方案1:使用synchronized
synchronized(this) {
count1++;
count2++;
}
// ✅ 方案2:合并为对象
class CountPair {
int count1, count2;
}
AtomicReference<CountPair> pair = new AtomicReference<>();
5.4 ABA问题及解决方案
ABA问题的产生场景
典型场景:无锁栈的实现
在实现无锁数据结构(如栈、队列)时,ABA问题特别容易出现。
问题示例(无锁栈):
// 简化版无锁栈
AtomicReference<Node> head = new AtomicReference<>();
// 出栈操作
public Node pop() {
Node oldHead;
Node newHead;
do {
oldHead = head.get(); // 1. 读取头节点A
if (oldHead == null) {
return null;
}
newHead = oldHead.next; // 2. 准备设置新的头节点
// 问题:如果在步骤1和3之间,head从A变成B再变回A
// CAS仍然会成功,但实际上头节点已经被换过了!
} while (!head.compareAndSet(oldHead, newHead)); // 3. CAS更新
return oldHead;
}
ABA问题的时间线:
T1: 线程1读取 head = A
T2: 线程2执行:head = A -> B -> A(先push再pop,又回到A)
T3: 线程1执行CAS(A, newHead)
结果:CAS成功!但此时A已经不是原来的A了
为什么会有问题?
- 虽然head的值还是A,但A指向的节点可能已经被修改过
- 可能导致数据丢失或逻辑错误
- CAS无法检测到"值被换过"这个事实
版本号机制(解决思路)
核心思想: 在值的基础上增加版本号,每次修改版本号递增
生活化理解:
- 就像给每次修改打上时间戳
- 即使值相同,版本号也不同
- CAS时同时检查值和版本号
实现思路:
// 伪代码说明
class VersionedValue {
Object value; // 实际值
int version; // 版本号
}
// CAS操作
boolean compareAndSet(Object expectedValue, int expectedVersion,
Object newValue, int newVersion) {
if (currentValue == expectedValue && currentVersion == expectedVersion) {
// 值和版本号都匹配,才更新
value = newValue;
version = newVersion + 1; // 版本号递增
return true;
}
return false;
}
优点:
- ✅ 能检测到ABA问题
- ✅ 版本号递增,不会重复
- ✅ 精确控制每次修改
缺点:
- ⚠️ 需要额外的存储空间(版本号)
- ⚠️ 实现稍微复杂一些
AtomicStampedReference(版本号解决方案)
AtomicStampedReference:Java提供的带版本号的原子引用类,可以解决ABA问题。
使用方式:
import java.util.concurrent.atomic.AtomicStampedReference;
// 创建:初始值为"A",版本号为0
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0);
// 获取值和版本号
int[] stampHolder = new int[1]; // 用于接收版本号的数组
String value = ref.get(stampHolder);
int version = stampHolder[0]; // 当前版本号
// CAS操作:同时比较值和版本号
boolean success = ref.compareAndSet(
"A", "B", // 期望值和新值
0, 1 // 期望版本号和新版本号
);
// 设置值和版本号
ref.set("C", 2); // 设置值为"C",版本号为2
解决ABA问题的示例:
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 0);
// 线程1:准备修改
Thread t1 = new Thread(() -> {
int[] stamp = new int[1];
String old = ref.get(stamp); // 获取值和版本号
int oldVersion = stamp[0]; // version = 0
Thread.sleep(1000);
// CAS:同时检查值和版本号
boolean success = ref.compareAndSet(
old, "C",
oldVersion, oldVersion + 1
);
// 如果线程2改过,版本号已经不是0了,CAS失败 ✅
});
// 线程2:A -> B -> A
Thread t2 = new Thread(() -> {
int[] stamp = new int[1];
String current = ref.get(stamp);
// A -> B,版本号 0 -> 1
ref.compareAndSet(current, "B", stamp[0], stamp[0] + 1);
// B -> A,版本号 1 -> 2
current = ref.get(stamp);
ref.compareAndSet(current, "A", stamp[0], stamp[0] + 1);
// 此时值还是A,但版本号已经是2了
});
t1.start();
t2.start();
// 结果:线程1的CAS失败,因为版本号不匹配 ✅
核心方法:
// 获取值和版本号
V get(int[] stampHolder) // 版本号通过数组返回
// 比较并设置(同时比较值和版本号)
boolean compareAndSet(V expectedValue, V newValue,
int expectedStamp, int newStamp)
// 设置值和版本号
void set(V newValue, int newStamp)
AtomicMarkableReference(标记位解决方案)
AtomicMarkableReference:使用boolean标记代替版本号,更节省内存。
适用场景:
- 只需要知道值是否被修改过(不需要知道修改了几次)
- 对精度要求不高
- 想节省内存(boolean比int小)
使用方式:
import java.util.concurrent.atomic.AtomicMarkableReference;
// 创建:初始值为"A",标记为false
AtomicMarkableReference<String> ref =
new AtomicMarkableReference<>("A", false);
// 获取值和标记
boolean[] markHolder = new boolean[1];
String value = ref.get(markHolder);
boolean mark = markHolder[0]; // 当前标记
// CAS操作:同时比较值和标记
boolean success = ref.compareAndSet(
"A", "B", // 期望值和新值
false, true // 期望标记和新标记
);
示例:
AtomicMarkableReference<String> ref =
new AtomicMarkableReference<>("A", false);
// 线程1
Thread t1 = new Thread(() -> {
boolean[] mark = new boolean[1];
String old = ref.get(mark);
boolean oldMark = mark[0]; // false
Thread.sleep(1000);
// CAS:检查值和标记
ref.compareAndSet(old, "C", oldMark, true);
// 如果线程2改过,标记已经不是false了,CAS失败
});
// 线程2:修改值并改变标记
Thread t2 = new Thread(() -> {
boolean[] mark = new boolean[1];
String current = ref.get(mark);
// 修改值,标记 false -> true
ref.compareAndSet(current, "B", false, true);
// 再改回来,标记 true -> false
ref.compareAndSet("B", "A", true, false);
});
对比总结:
| 特性 | AtomicStampedReference | AtomicMarkableReference |
|---|---|---|
| 标记类型 | int(版本号) | boolean(标记) |
| 精度 | ✅ 高(知道修改次数) | ⚠️ 低(只知道是否修改过) |
| 内存占用 | ⚠️ 较大(int 4字节) | ✅ 较小(boolean 1字节) |
| 适用场景 | 需要精确版本控制 | 只需要标记是否修改 |
| 推荐使用 | 大多数场景 | 内存敏感、精度要求不高的场景 |
选择建议:
- 大多数情况下使用 AtomicStampedReference(更精确)
- 如果只需要标记是否修改过,且内存紧张,使用 AtomicMarkableReference
第六章 AQS(AbstractQueuedSynchronizer)
6.1 AQS概述
AQS的设计思想
**AQS(AbstractQueuedSynchronizer)**是JUC包中实现同步器的基础框架,很多同步工具类都是基于AQS实现的。
核心思想:
- 使用一个volatile int state表示同步状态
- 使用FIFO队列管理等待线程
- 通过CAS操作更新状态
- 通过模板方法模式,子类实现具体的同步逻辑
设计模式:
- 模板方法模式:定义算法骨架,子类实现具体步骤
- 状态模式:根据state的不同值,执行不同的逻辑
AQS的核心数据结构
主要组成:
-
state(同步状态)
private volatile int state; // volatile保证可见性 -
等待队列(CLH队列)
// 队列头节点(虚拟节点) private transient volatile Node head; // 队列尾节点 private transient volatile Node tail; -
Node节点
static final class Node { static final Node SHARED = new Node(); // 共享模式 static final Node EXCLUSIVE = null; // 独占模式 static final int CANCELLED = 1; // 取消状态 static final int SIGNAL = -1; // 需要唤醒 static final int CONDITION = -2; // 在条件队列中 static final int PROPAGATE = -3; // 传播状态 volatile int waitStatus; // 等待状态 volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 线程引用 Node nextWaiter; // 下一个等待节点 }
CLH队列
CLH队列的特点:
- **CLH(Craig, Landin, and Hagersten)**是一种自旋锁队列
- AQS对CLH队列进行了改进,使用双向链表
- 使用虚拟头节点(head)简化操作
- 节点通过CAS操作入队和出队
队列结构:
head (虚拟节点)
↓
Node1 ←→ Node2 ←→ Node3 (tail)
↑ ↑ ↑
Thread1 Thread2 Thread3
6.2 AQS的核心方法
tryAcquire/tryRelease(独占模式)
独占模式(Exclusive): 同一时刻只有一个线程能获取锁。
理解要点:
- 独占模式就像只有一个座位的会议室
- 其他线程必须等待当前线程释放锁
- 典型应用:ReentrantLock
tryAcquire(尝试获取锁)
方法定义: 子类需要实现这个方法,定义如何获取锁。
// AQS中的抽象方法,子类必须实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
ReentrantLock的实现逻辑(简化理解):
protected boolean tryAcquire(int acquires) {
int state = getState(); // 获取当前状态
if (state == 0) {
// 锁空闲,尝试CAS获取
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true; // 获取成功
}
} else if (isCurrentThreadOwner()) {
// 可重入:当前线程已持有锁,state+1
setState(state + 1);
return true;
}
return false; // 获取失败
}
简单理解:
- 检查锁是否空闲(state == 0)
- 如果空闲,CAS尝试获取锁
- 如果当前线程已持有锁,支持可重入
- 返回true表示获取成功,false表示失败
tryRelease(尝试释放锁)
方法定义: 子类需要实现这个方法,定义如何释放锁。
// AQS中的抽象方法,子类必须实现
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
ReentrantLock的实现逻辑(简化理解):
protected boolean tryRelease(int releases) {
int newState = getState() - releases; // 状态减1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 只有持有锁的线程才能释放
if (newState == 0) {
// 完全释放锁
setExclusiveOwnerThread(null);
setState(0);
return true;
} else {
// 还有重入次数,只更新state
setState(newState);
return false;
}
}
简单理解:
- 检查是否是持有锁的线程(防止非法释放)
- state减1
- 如果state变为0,完全释放锁
- 返回true表示锁已完全释放,false表示还有重入次数
tryAcquireShared/tryReleaseShared(共享模式)
共享模式(Shared): 多个线程可以同时获取锁。
理解要点:
- 共享模式就像图书馆,多个人可以同时进入
- state表示可用资源数量
- 典型应用:Semaphore(信号量)、ReadWriteLock的读锁
tryAcquireShared(尝试获取共享锁)
方法定义: 子类需要实现这个方法,定义如何获取共享锁。
返回值含义:
- 负数:获取失败
- 0:获取成功,但没有剩余资源了
- 正数:获取成功,还有剩余资源
// AQS中的抽象方法
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
Semaphore的实现逻辑(简化理解):
protected int tryAcquireShared(int acquires) {
int available = getState(); // 获取可用许可证数
int remaining = available - acquires; // 计算剩余数量
// 资源不足(remaining < 0)或CAS成功
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining; // 返回剩余数量
}
简单理解:
- state表示可用资源数量(如Semaphore的许可证数)
- 尝试获取指定数量的资源
- 如果资源足够,CAS更新state
- 返回剩余资源数量
tryReleaseShared(尝试释放共享锁)
方法定义: 子类需要实现这个方法,定义如何释放共享锁。
// AQS中的抽象方法
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
Semaphore的实现逻辑(简化理解):
protected boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState(); // 当前资源数
int next = current + releases; // 释放后的资源数
if (compareAndSetState(current, next))
return true; // 释放成功
// CAS失败,自旋重试
}
}
简单理解:
- 释放指定数量的资源
- state增加
- 使用CAS更新,失败则自旋重试
- 返回true表示释放成功
acquire/acquireShared(获取锁的核心流程)
这些方法是AQS提供的模板方法,实现了完整的获取锁流程。
acquire(独占模式获取锁)
方法作用: 独占模式下获取锁的完整流程,包括尝试获取、入队、阻塞等。
public final void acquire(int arg) {
// 步骤1:尝试获取锁
if (!tryAcquire(arg) &&
// 步骤2:失败则加入队列并自旋尝试
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 步骤3:如果被中断,恢复中断状态
selfInterrupt();
}
执行流程详解:
1. tryAcquire(arg)
├─ 成功 → 直接返回,获取锁成功
└─ 失败 ↓
2. addWaiter(Node.EXCLUSIVE)
└─ 创建节点,加入等待队列
3. acquireQueued(node, arg)
├─ 自旋尝试获取锁
├─ 成功 → 返回false,获取锁成功
└─ 失败 → 阻塞线程,等待被唤醒
4. selfInterrupt()
└─ 如果被中断过,恢复中断状态
简单理解:
- 先尝试快速获取锁(tryAcquire)
- 失败则加入等待队列
- 在队列中自旋尝试,还不行就阻塞
- 等待其他线程释放锁后唤醒
acquireShared(共享模式获取锁)
方法作用: 共享模式下获取锁的完整流程。
public final void acquireShared(int arg) {
// tryAcquireShared返回值 < 0 表示获取失败
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg); // 失败则进入共享模式获取流程
}
简单理解:
- 尝试获取共享资源
- 返回值小于0表示资源不足
- 失败则进入队列等待
release/releaseShared(释放锁的核心流程)
这些方法实现了完整的释放锁流程,包括唤醒等待线程。
release(独占模式释放锁)
方法作用: 独占模式下释放锁,并唤醒等待队列中的线程。
public final boolean release(int arg) {
if (tryRelease(arg)) { // 步骤1:尝试释放锁
Node h = head;
// 步骤2:如果队列中有等待的线程,唤醒下一个
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false; // 释放失败(还有重入次数)
}
执行流程详解:
1. tryRelease(arg)
├─ 成功(完全释放) → 继续下一步
└─ 失败(还有重入) → 返回false
2. unparkSuccessor(h)
└─ 唤醒等待队列中的下一个线程
简单理解:
- 尝试释放锁
- 如果完全释放,唤醒队列中的下一个线程
- 让等待的线程有机会获取锁
releaseShared(共享模式释放锁)
方法作用: 共享模式下释放资源,并唤醒等待的线程。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 尝试释放资源
doReleaseShared(); // 唤醒等待的线程(可能多个)
return true;
}
return false;
}
简单理解:
- 释放共享资源
- 唤醒所有等待该资源的线程
- 多个线程可能同时被唤醒(因为共享模式允许多个线程同时获取)
6.3 AQS的实现原理
状态变量state
state的作用:
- 表示同步状态
- 在不同同步器中有不同含义:
- ReentrantLock:表示重入次数
- Semaphore:表示可用许可证数量
- CountDownLatch:表示计数器值
- ReentrantReadWriteLock:高16位表示读锁,低16位表示写锁
state的访问:
// 获取state
protected final int getState() {
return state;
}
// 设置state
protected final void setState(int newState) {
state = newState;
}
// CAS更新state
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
节点Node结构
Node节点详解:
static final class Node {
// 标记节点为共享模式
static final Node SHARED = new Node();
// 标记节点为独占模式
static final Node EXCLUSIVE = null;
// 等待状态值
static final int CANCELLED = 1; // 节点已取消
static final int SIGNAL = -1; // 后继节点需要被唤醒
static final int CONDITION = -2; // 节点在条件队列中等待
static final int PROPAGATE = -3; // 共享模式下需要传播
// 等待状态(volatile保证可见性)
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 节点对应的线程
volatile Thread thread;
// 下一个等待节点(用于条件队列)
Node nextWaiter;
}
入队和出队操作
入队(addWaiter)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 快速路径:队列不为空,CAS添加到队尾
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 慢速路径:队列为空或CAS失败,完整入队
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 队列为空,初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
出队(unparkSuccessor)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 找到下一个需要唤醒的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒线程
}
自旋和阻塞
acquireQueued(自旋获取锁)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 前驱是head,尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 获取失败,检查是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
parkAndCheckInterrupt(阻塞)
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程
return Thread.interrupted(); // 返回中断状态并清除
}
6.4 基于AQS的实现类
ReentrantLock
ReentrantLock基于AQS的独占模式实现。
public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 实现tryAcquire和tryRelease
}
static final class NonfairSync extends Sync {
// 非公平锁实现
}
static final class FairSync extends Sync {
// 公平锁实现
}
}
ReentrantReadWriteLock
ReentrantReadWriteLock基于AQS的共享模式和独占模式实现。
public class ReentrantReadWriteLock implements ReadWriteLock {
private final ReadLock readerLock;
private final WriteLock writerLock;
// 使用AQS的state:
// 高16位:读锁计数
// 低16位:写锁计数
}
Semaphore
Semaphore基于AQS的共享模式实现。
public class Semaphore {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// state表示可用许可证数量
// tryAcquireShared:获取许可证
// tryReleaseShared:释放许可证
}
}
CountDownLatch
CountDownLatch基于AQS的共享模式实现。
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
// state表示计数器值
// countDown:state - 1
// await:等待state == 0
}
}
CyclicBarrier
CyclicBarrier基于ReentrantLock和Condition实现。
public class CyclicBarrier {
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
// 使用ReentrantLock和Condition实现
}
第七章 Lock接口与ReentrantLock
7.1 Lock接口
Lock接口的方法
Lock接口提供了比synchronized更灵活的锁操作。
public interface Lock {
void lock(); // 获取锁(阻塞)
void lockInterruptibly() throws InterruptedException; // 可中断获取锁
boolean tryLock(); // 尝试获取锁(不阻塞)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时获取锁
void unlock(); // 释放锁
Condition newCondition(); // 获取条件对象
}
方法说明:
- lock() - 最常用的方法,获取锁,如果锁被占用则阻塞等待
- lockInterruptibly() - 可中断的获取锁,线程在等待时可以被中断
- tryLock() - 尝试获取锁,立即返回true/false,不会阻塞
- tryLock(time, unit) - 在指定时间内尝试获取锁,超时返回false
- unlock() - 释放锁,必须在finally块中调用
- newCondition() - 创建Condition对象,用于线程间的协调
Lock vs synchronized
| 特性 | synchronized | Lock |
|---|---|---|
| 获取锁方式 | 自动获取和释放 | 手动获取和释放 |
| 可中断 | ❌ 不可中断 | ✅ 可中断(lockInterruptibly) |
| 超时获取 | ❌ 不支持 | ✅ 支持(tryLock) |
| 公平锁 | ❌ 非公平 | ✅ 可选公平/非公平 |
| 多个条件 | ❌ 只有一个条件 | ✅ 可以有多个Condition |
| 使用复杂度 | 简单 | 较复杂(需要finally释放) |
使用示例对比:
// synchronized方式(简单但功能有限)
public synchronized void method() {
// 同步代码
}
// Lock方式(更灵活)
private Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 同步代码
} finally {
lock.unlock(); // ⚠️ 必须在finally中释放,防止死锁
}
}
// 可中断的获取锁
public void interruptibleMethod() throws InterruptedException {
lock.lockInterruptibly(); // 等待时可以响应中断
try {
// 同步代码
} finally {
lock.unlock();
}
}
// 尝试获取锁(不阻塞)
public void tryLockMethod() {
if (lock.tryLock()) { // 立即返回,不等待
try {
// 同步代码
} finally {
lock.unlock();
}
} else {
// 获取锁失败,执行其他逻辑
}
}
7.2 ReentrantLock
可重入性
可重入锁: 同一个线程可以多次获取同一把锁。
理解要点:
- 就像一把钥匙可以打开同一扇门多次
- 避免死锁:方法A调用方法B,两个方法都需要同一把锁
- ReentrantLock是可重入的,synchronized也是可重入的
示例代码:
ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock(); // 第一次获取锁
try {
method2(); // 调用method2,可以再次获取同一把锁
} finally {
lock.unlock(); // 第一次释放
}
}
public void method2() {
lock.lock(); // 第二次获取锁(可重入)
try {
// 执行任务
} finally {
lock.unlock(); // 第二次释放
}
}
实现原理(简单理解):
- 使用state记录重入次数
- 每次lock(),state + 1
- 每次unlock(),state - 1
- state == 0时,锁被完全释放
公平锁与非公平锁
核心区别: 获取锁的顺序不同
非公平锁(默认)
特点: 新来的线程可能"插队",直接尝试获取锁
ReentrantLock lock = new ReentrantLock(); // 默认非公平锁
// 或
ReentrantLock lock = new ReentrantLock(false); // 显式指定非公平锁
工作原理:
// 非公平锁:先尝试直接获取锁,失败才排队
lock() {
if (CAS尝试直接获取锁) { // 新线程可能插队
return; // 成功
}
加入队列等待; // 失败才排队
}
优缺点:
- ✅ 性能更好(减少线程切换)
- ✅ 吞吐量更高
- ❌ 可能导致线程饥饿(某些线程一直获取不到锁)
适用场景: 大多数场景推荐使用非公平锁
公平锁
特点: 严格按照等待时间顺序获取锁,先来先服务
ReentrantLock lock = new ReentrantLock(true); // 公平锁
工作原理:
// 公平锁:先检查队列,有等待的线程就排队
lock() {
if (队列中有等待的线程) {
加入队列等待; // 不插队
} else {
尝试获取锁;
}
}
优缺点:
- ✅ 公平性保证,避免饥饿
- ✅ 等待时间长的线程优先获得锁
- ❌ 性能较差(更多上下文切换)
- ❌ 吞吐量较低
适用场景: 需要严格公平性的场景
性能对比总结:
- 非公平锁:性能好(约快10-20%),适合大多数场景
- 公平锁:性能差,但更公平,适合对公平性要求高的场景
- 选择建议:除非有特殊需求,否则使用非公平锁
7.3 Condition接口
Condition的作用
Condition提供了类似Object.wait/notify的线程等待和唤醒机制,但功能更强大。
理解要点:
- Condition是Lock的等待/通知机制
- 一个Lock可以创建多个Condition
- 比wait/notify更灵活、更精确
Condition接口方法:
public interface Condition {
void await() throws InterruptedException; // 等待,可中断
void awaitUninterruptibly(); // 等待,不可中断
long awaitNanos(long nanosTimeout) throws InterruptedException; // 超时等待(纳秒)
boolean await(long time, TimeUnit unit) throws InterruptedException; // 超时等待
boolean awaitUntil(Date deadline) throws InterruptedException; // 等待到指定时间
void signal(); // 唤醒一个等待线程
void signalAll(); // 唤醒所有等待线程
}
await()方法(等待条件)
作用: 让当前线程等待,直到被signal唤醒
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待条件
lock.lock();
try {
condition.await(); // 释放锁并等待,被唤醒后重新获取锁
} finally {
lock.unlock();
}
执行流程(简单理解):
- 当前线程加入条件等待队列
- 释放锁
- 阻塞等待
- 被signal唤醒后,重新获取锁
- 继续执行
signal()/signalAll()方法(唤醒等待线程)
signal(): 唤醒一个等待线程(类似notify)
signalAll(): 唤醒所有等待线程(类似notifyAll)
// 唤醒一个等待线程
lock.lock();
try {
condition.signal(); // 唤醒一个等待的线程
} finally {
lock.unlock();
}
// 唤醒所有等待线程
lock.lock();
try {
condition.signalAll(); // 唤醒所有等待的线程
} finally {
lock.unlock();
}
Condition vs wait/notify
| 特性 | wait/notify | Condition |
|---|---|---|
| 前置条件 | 必须在synchronized块中 | 必须先获取Lock |
| 多个条件 | ❌ 不支持 | ✅ 支持(一个Lock多个Condition) |
| 可中断 | ✅ 支持 | ✅ 支持 |
| 超时等待 | ✅ 有限支持 | ✅ 更灵活的超时 |
| 使用场景 | 简单的等待/通知 | 复杂的同步控制 |
优势总结:
- ✅ 多个条件:可以创建多个Condition,精确控制不同条件的等待/唤醒
- ✅ 更灵活:超时等待、中断控制更强大
- ✅ 性能更好:避免不必要的线程唤醒
生产者消费者模式实现
使用Condition的优势: 可以为"队列满"和"队列空"创建不同的Condition
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 队列不满的条件
Condition notEmpty = lock.newCondition(); // 队列不空的条件
// 生产者:等待队列不满,通知队列不空
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 等待队列不满
// 生产...
notEmpty.signal(); // 通知消费者:队列不空了
} finally {
lock.unlock();
}
}
// 消费者:等待队列不空,通知队列不满
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 等待队列不空
// 消费...
notFull.signal(); // 通知生产者:队列不满了
return item;
} finally {
lock.unlock();
}
}
优势:
- 精确唤醒:只唤醒需要等待该条件的线程
- 避免虚假唤醒:使用while循环检查条件
- 性能更好:不需要唤醒所有线程
7.4 ReentrantLock的实现原理
基于AQS的实现
ReentrantLock内部结构(简化理解):
public class ReentrantLock implements Lock {
private final Sync sync; // 内部同步器,继承自AQS
// 公平锁和非公平锁都继承自Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock(); // 由子类实现(公平/非公平)
// 尝试获取锁(非公平方式)
boolean nonfairTryAcquire(int acquires) {
// 1. 检查锁是否空闲
// 2. CAS尝试获取锁
// 3. 支持可重入
}
// 释放锁
boolean tryRelease(int releases) {
// 1. 检查是否是持有锁的线程
// 2. state减1
// 3. 如果state为0,完全释放
}
}
}
简单理解:
- ReentrantLock内部使用AQS实现
- 公平锁和非公平锁分别实现不同的获取策略
- 所有锁操作最终都调用AQS的方法
公平锁的获取流程
核心区别:获取锁前先检查队列
// 公平锁:先检查队列,有等待的就不插队
lock() {
acquire(1); // 调用AQS的acquire
}
tryAcquire(1) {
if (锁空闲) {
if (队列中有等待的线程) {
return false; // 有等待的,不插队
}
CAS获取锁;
return true;
}
// 可重入处理...
}
流程总结:
- 检查锁是否空闲
- 关键:检查队列中是否有等待的线程
- 如果有等待的,不插队,返回false,加入队列等待
- 如果没有等待的,CAS获取锁
非公平锁的获取流程
核心区别:新线程可能插队
// 非公平锁:先尝试直接获取,失败才排队
lock() {
if (CAS直接尝试获取锁) { // 新线程可能插队
return; // 成功
}
acquire(1); // 失败才调用AQS的acquire
}
tryAcquire(1) {
if (锁空闲) {
CAS获取锁; // 不检查队列,直接尝试
return true/false;
}
// 可重入处理...
}
流程总结:
- 关键:先直接CAS尝试获取锁(可能插队)
- 失败才调用acquire,进入队列
- 在队列中也直接尝试获取,不检查是否轮到
锁的释放流程
释放流程(公平锁和非公平锁相同):
unlock() {
release(1); // 调用AQS的release
}
release(1) {
if (tryRelease(1)) { // 尝试释放锁
if (队列中有等待的线程) {
唤醒下一个等待的线程; // 让等待的线程有机会获取锁
}
return true;
}
return false; // 还有重入次数,未完全释放
}
流程总结:
- state减1
- 如果state变为0,完全释放锁
- 唤醒队列中等待的下一个线程
- 让等待的线程有机会获取锁
第八章 读写锁(ReadWriteLock)
8.1 ReadWriteLock接口
读写锁的设计思想
ReadWriteLock提供了两种锁:
- 读锁(ReadLock):共享锁,多个线程可以同时持有
- 写锁(WriteLock):独占锁,同一时刻只有一个线程能持有
设计目的:
- 读操作多、写操作少的场景
- 提高并发性能
- 读操作不互斥,写操作互斥
读锁与写锁的关系
锁的兼容性:
| 当前锁状态 | 读锁 | 写锁 |
|---|---|---|
| 无锁 | ✅ 可以获取 | ✅ 可以获取 |
| 读锁 | ✅ 可以获取(多个读锁共享) | ❌ 不能获取(等待读锁释放) |
| 写锁 | ❌ 不能获取(等待写锁释放) | ❌ 不能获取(等待写锁释放) |
规则:
- 多个读锁可以同时持有
- 读锁和写锁互斥
- 写锁和写锁互斥
8.2 ReentrantReadWriteLock
理解要点:
- 读锁(ReadLock):共享锁,多个线程可以同时持有(类似多人同时看书)
- 写锁(WriteLock):独占锁,同一时刻只有一个线程能持有(类似一人独占写作)
读锁的共享性
读锁基于AQS的共享模式实现,允许多个线程同时读取。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReadLock readLock = rwLock.readLock();
// 多个线程可以同时获取读锁
readLock.lock();
try {
// 读取数据,多个线程可以同时执行这里
} finally {
readLock.unlock();
}
特点:
- ✅ 多个线程可以同时持有读锁
- ✅ 读锁是可重入的
- ❌ 读锁会阻塞写锁的获取(有读锁时不能获取写锁)
适用场景: 读多写少的场景,如缓存、配置读取
写锁的排他性
写锁基于AQS的独占模式实现,同一时刻只有一个线程能写入。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
WriteLock writeLock = rwLock.writeLock();
// 同一时刻只有一个线程能获取写锁
writeLock.lock();
try {
// 写入数据,同一时刻只有一个线程执行这里
} finally {
writeLock.unlock();
}
特点:
- ✅ 同一时刻只有一个线程能持有写锁
- ❌ 写锁会阻塞所有读锁和写锁
- ✅ 写锁是可重入的
适用场景: 数据写入、更新操作
锁降级(写锁→读锁)
锁降级: 将写锁降级为读锁(支持)
理解要点:
- 在持有写锁的情况下,先获取读锁,再释放写锁
- 这样可以保证数据一致性,同时允许其他线程读取
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReadLock readLock = rwLock.readLock();
WriteLock writeLock = rwLock.writeLock();
writeLock.lock(); // 1. 先获取写锁
try {
// 更新数据
readLock.lock(); // 2. 在持有写锁的情况下获取读锁
} finally {
writeLock.unlock(); // 3. 释放写锁(此时还持有读锁)
}
// 现在持有读锁,可以读取数据
try {
// 读取数据
} finally {
readLock.unlock(); // 4. 释放读锁
}
注意事项:
- ⚠️ 必须在持有写锁的情况下获取读锁
- ⚠️ 先获取读锁,再释放写锁(顺序很重要)
- ❌ 不能先释放写锁,再获取读锁(中间可能被其他线程获取写锁)
锁升级(读锁→写锁)
锁升级: 将读锁升级为写锁(不支持)
理解要点:
- 不能在持有读锁的情况下直接获取写锁
- 会导致死锁:写锁等待读锁释放,但读锁不会释放
// ❌ 错误:会导致死锁
readLock.lock();
try {
writeLock.lock(); // 会一直阻塞,因为还在持有读锁
} finally {
readLock.unlock();
}
// ✅ 正确:先释放读锁,再获取写锁
readLock.lock();
try {
// 读取数据
} finally {
readLock.unlock(); // 先释放读锁
}
writeLock.lock(); // 再获取写锁
try {
// 写入数据
} finally {
writeLock.unlock();
}
为什么不能升级:
- 如果多个线程都持有读锁,都尝试升级为写锁
- 每个线程都在等待其他线程释放读锁
- 形成死锁:所有线程都在等待,但谁也不释放
8.3 ReentrantReadWriteLock的实现
高16位存储读锁状态
state的拆分:
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 获取读锁数量(高16位)
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写锁数量(低16位)
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
state结构:
state (32位)
├── 高16位:读锁计数(最多65535个读锁)
└── 低16位:写锁计数(重入次数)
低16位存储写锁状态
写锁的获取:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c); // 获取写锁计数
if (c != 0) {
// 有读锁或其他线程持有写锁
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 可重入
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
// 尝试获取写锁
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
读锁的获取和释放
读锁的获取:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果有写锁且不是当前线程持有,失败
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c); // 读锁计数
if (!readerShouldBlock() && r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 第一次获取读锁
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 使用ThreadLocal存储每个线程的读锁计数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
读锁的释放:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
写锁的获取和释放
写锁的获取: 见上面的tryAcquire方法
写锁的释放:
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
8.4 StampedLock
StampedLock的特点
StampedLock是JDK 8引入的新锁,提供了三种模式:
- 写锁(Writing):独占锁,类似ReentrantReadWriteLock的写锁
- 悲观读锁(Reading):共享锁,类似ReentrantReadWriteLock的读锁
- 乐观读锁(Optimistic Reading):不阻塞,通过验证戳记来检查数据是否被修改
特点:
- 不支持重入
- 不支持Condition
- 性能优于ReentrantReadWriteLock(特别是在读多写少的场景)
乐观读
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private final StampedLock lock = new StampedLock();
private double x, y;
public double distanceFromOrigin() {
// 1. 尝试乐观读
long stamp = lock.tryOptimisticRead();
double curX = x, curY = y;
// 2. 验证戳记是否有效(检查是否有写操作)
if (!lock.validate(stamp)) {
// 3. 戳记无效,升级为悲观读锁
stamp = lock.readLock();
try {
curX = x;
curY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(curX * curX + curY * curY);
}
}
乐观读的流程:
tryOptimisticRead():获取乐观读戳记,不阻塞- 读取数据
validate(stamp):验证戳记是否有效- 有效:说明没有写操作,读取成功
- 无效:说明有写操作,升级为悲观读锁
悲观读
public double read() {
// 获取悲观读锁
long stamp = lock.readLock();
try {
return x + y;
} finally {
lock.unlockRead(stamp);
}
}
悲观读锁:
- 类似ReentrantReadWriteLock的读锁
- 多个线程可以同时持有
- 与写锁互斥
写锁
public void move(double deltaX, double deltaY) {
// 获取写锁
long stamp = lock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
写锁:
- 独占锁,同一时刻只有一个线程能持有
- 与读锁和写锁都互斥
性能对比
性能测试:
public class LockPerformanceComparison {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private StampedLock stampedLock = new StampedLock();
// ReentrantReadWriteLock
public void readWithRWLock() {
rwLock.readLock().lock();
try {
// 读操作
} finally {
rwLock.readLock().unlock();
}
}
// StampedLock乐观读
public void readWithStampedLock() {
long stamp = stampedLock.tryOptimisticRead();
// 读操作
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock();
try {
// 读操作
} finally {
stampedLock.unlockRead(stamp);
}
}
}
}
性能特点:
- 读多写少:StampedLock(乐观读)性能最好
- 写多读少:性能接近
- 读操作占比高:StampedLock优势明显(减少锁竞争)
使用建议:
- 读多写少:优先使用StampedLock
- 需要重入:使用ReentrantReadWriteLock
- 需要Condition:使用ReentrantReadWriteLock
第九章 原子类(Atomic)
9.1 原子类概述
原子类的分类
Java中的原子类分为以下几类:
1. 基本类型原子类
AtomicInteger- 原子整型AtomicLong- 原子长整型AtomicBoolean- 原子布尔型
2. 数组类型原子类
AtomicIntegerArray- 原子整型数组AtomicLongArray- 原子长整型数组AtomicReferenceArray- 原子引用数组
3. 引用类型原子类
AtomicReference- 原子引用AtomicStampedReference- 带版本号的原子引用(解决ABA问题)AtomicMarkableReference- 带标记位的原子引用
4. 字段更新器
AtomicIntegerFieldUpdater- 整型字段更新器AtomicLongFieldUpdater- 长整型字段更新器AtomicReferenceFieldUpdater- 引用类型字段更新器
5. 累加器类(JDK 8+)
LongAdder- 长整型累加器LongAccumulator- 长整型累加器DoubleAdder- 双精度累加器DoubleAccumulator- 双精度累加器
原子类的优势
1. 无锁编程
// 传统方式(需要锁)
private int count = 0;
public synchronized void increment() {
count++;
}
// 原子类方式(无锁)
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
2. 高性能
- 使用CAS操作,避免线程阻塞
- 在低竞争场景下性能优于synchronized
- 适合高并发场景
3. 线程安全
- 所有操作都是原子性的
- 保证线程安全,无需额外的同步机制
4. 简单易用
- API简洁明了
- 不需要手动管理锁
9.2 基本类型原子类
AtomicInteger
AtomicInteger常用方法:
AtomicInteger count = new AtomicInteger(0);
// 基本操作
count.get(); // 获取值
count.set(10); // 设置值
count.getAndSet(20); // 获取旧值并设置新值
count.compareAndSet(20, 30); // 比较并设置
// 自增自减
count.incrementAndGet(); // ++count,返回新值
count.getAndIncrement(); // count++,返回旧值
count.decrementAndGet(); // --count,返回新值
count.getAndDecrement(); // count--,返回旧值
// 加减操作
count.addAndGet(5); // 加5,返回新值
count.getAndAdd(10); // 返回旧值,再加10
// 函数式更新(JDK 8+)
count.updateAndGet(x -> x * 2); // 原子更新
count.getAndUpdate(x -> x / 2); // 获取并更新
简单应用示例:
// 线程安全的计数器
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作,无需锁
}
AtomicLong
AtomicLong与AtomicInteger类似,用于长整型。
AtomicLong total = new AtomicLong(0L);
total.addAndGet(100); // 增加100
total.get(); // 获取值
注意:
- 在32位JVM上,long的读写不是原子的
- AtomicLong保证long类型的原子操作
- JDK 8+推荐使用LongAdder,性能更好
AtomicBoolean
AtomicBoolean用于布尔类型的原子操作。
AtomicBoolean flag = new AtomicBoolean(false);
flag.get(); // 获取值
flag.set(true); // 设置值
flag.compareAndSet(false, true); // 比较并设置
flag.getAndSet(false); // 获取并设置
flag.lazySet(true); // 延迟设置
状态标志示例:
private AtomicBoolean running = new AtomicBoolean(true);
public void shutdown() {
running.set(false);
}
public void doWork() {
while (running.get()) {
// 执行任务
}
}
常用方法总结
所有基本类型原子类都提供以下方法:
| 方法 | 说明 | 返回值 |
|---|---|---|
get() | 获取当前值 | 当前值 |
set(int newValue) | 设置新值 | void |
getAndSet(int newValue) | 获取当前值并设置新值 | 旧值 |
compareAndSet(int expect, int update) | 比较并设置 | boolean |
lazySet(int newValue) | 延迟设置(最终一致性) | void |
getAndIncrement() | 先返回再自增 | 旧值 |
incrementAndGet() | 先自增再返回 | 新值 |
getAndDecrement() | 先返回再自减 | 旧值 |
decrementAndGet() | 先自减再返回 | 新值 |
getAndAdd(int delta) | 先返回再加 | 旧值 |
addAndGet(int delta) | 先加再返回 | 新值 |
9.3 数组类型原子类
AtomicIntegerArray
AtomicIntegerArray用于原子地更新数组中的元素。
AtomicIntegerArray array = new AtomicIntegerArray(10);
array.get(0); // 获取索引0的值
array.set(0, 10); // 设置索引0的值
array.getAndSet(0, 20); // 获取并设置
array.compareAndSet(0, 20, 30); // 比较并设置
array.incrementAndGet(0); // 索引0自增
array.addAndGet(0, 5); // 索引0加5
注意:
- 数组长度在创建时确定,不能改变
- 每个元素都是原子操作的
- 不同索引的元素可以并发访问
AtomicLongArray
AtomicLongArray与AtomicIntegerArray类似,用于长整型数组。
AtomicLongArray array = new AtomicLongArray(10);
array.addAndGet(0, 100); // 方法同AtomicIntegerArray
AtomicReferenceArray
AtomicReferenceArray用于引用类型数组的原子操作。
AtomicReferenceArray<String> array = new AtomicReferenceArray<>(10);
array.set(0, "Hello"); // 设置元素
array.get(0); // 获取元素
array.compareAndSet(0, "Hello", "World"); // 比较并设置
array.getAndSet(0, "Java"); // 获取并设置
9.4 引用类型原子类
AtomicReference
AtomicReference用于原子地更新引用类型变量。
AtomicReference<String> ref = new AtomicReference<>("初始值");
ref.get(); // 获取值
ref.set("新值"); // 设置值
ref.compareAndSet("新值", "更新值"); // 比较并设置
ref.getAndSet("最终值"); // 获取并设置
单例模式应用:
private static AtomicReference<Singleton> instance = new AtomicReference<>();
public static Singleton getInstance() {
Singleton current = instance.get();
if (current == null) {
current = new Singleton();
if (instance.compareAndSet(null, current)) {
return current;
}
return instance.get();
}
return current;
}
AtomicStampedReference
AtomicStampedReference通过版本号解决ABA问题。
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
// 获取值和版本号
int[] stampHolder = new int[1];
String value = ref.get(stampHolder);
int stamp = stampHolder[0];
// 比较并设置(同时比较值和版本号)
boolean success = ref.compareAndSet("A", "B", stamp, stamp + 1);
// 设置值和版本号
ref.set("C", stamp + 2);
AtomicMarkableReference
AtomicMarkableReference使用boolean标记代替版本号。
AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);
// 获取值和标记
boolean[] markHolder = new boolean[1];
String value = ref.get(markHolder);
boolean mark = markHolder[0];
// 比较并设置(同时比较值和标记)
boolean success = ref.compareAndSet("A", "B", false, true);
// 尝试设置标记
ref.attemptMark("C", true);
9.5 字段更新器
AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater用于原子地更新对象的整型字段。
// 字段必须是volatile类型
public class Counter {
private volatile int count = 0;
}
// 创建更新器
AtomicIntegerFieldUpdater<Counter> updater =
AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count");
// 使用更新器
Counter counter = new Counter();
updater.get(counter); // 获取值
updater.set(counter, 10); // 设置值
updater.incrementAndGet(counter); // 自增
updater.addAndGet(counter, 5); // 加5
使用限制:
- 字段必须是volatile类型
- 字段必须是可访问的(public或protected)
- 不能是static字段
- 不能是final字段
AtomicLongFieldUpdater
AtomicLongFieldUpdater用于长整型字段的原子更新。
public class Account {
private volatile long balance = 0;
}
AtomicLongFieldUpdater<Account> updater =
AtomicLongFieldUpdater.newUpdater(Account.class, "balance");
updater.addAndGet(account, 100); // 方法同AtomicIntegerFieldUpdater
AtomicReferenceFieldUpdater
AtomicReferenceFieldUpdater用于引用类型字段的原子更新。
public class Node {
private volatile Node next; // 必须是volatile
}
AtomicReferenceFieldUpdater<Node, Node> updater =
AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next");
Node node = new Node();
updater.get(node); // 获取值
updater.set(node, newNode); // 设置值
updater.compareAndSet(node, old, new); // 比较并设置
9.6 原子类的实现原理
CAS操作
原子类的核心是CAS操作。
// AtomicInteger的incrementAndGet实现(简化版)
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe的getAndAddInt实现
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 获取当前值
// CAS尝试更新,如果失败则自旋重试
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
Unsafe类的使用
原子类通过Unsafe类直接操作内存。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; // value字段的偏移量
static {
try {
// 获取value字段在对象中的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value; // 使用volatile保证可见性
public final boolean compareAndSet(int expect, int update) {
// 使用CAS操作原子地更新value字段
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
关键点:
- 使用
objectFieldOffset获取字段偏移量 - 使用
compareAndSwapInt进行CAS操作 - value字段使用volatile保证可见性
自旋机制
CAS失败后会自旋重试。
// 自旋实现示例
public final int incrementAndGet() {
int current;
int next;
do {
current = get(); // 1. 获取当前值
next = current + 1; // 2. 计算新值
// 3. CAS尝试更新,如果失败则自旋重试
} while (!compareAndSet(current, next));
return next;
}
自旋的优势:
- 避免线程阻塞和唤醒的开销
- 在低竞争场景下性能好
自旋的问题:
- 高竞争场景下会浪费CPU
- 可能导致CPU使用率100%
优化策略:
- JDK 8+提供了LongAdder等累加器,使用分段锁减少竞争
- 高竞争时可以使用synchronized
第十章 线程间通信
10.1 wait/notify/notifyAll
Object.wait()方法
wait()方法使当前线程进入等待状态,直到被其他线程唤醒。
private final Object lock = new Object();
// 等待
synchronized (lock) {
lock.wait(); // 释放锁,进入等待状态
}
// 唤醒
synchronized (lock) {
lock.notify(); // 唤醒一个等待的线程
}
wait()的要点:
- 必须在synchronized块中调用
- 调用wait()会释放锁
- 线程进入WAITING状态
- 被唤醒后需要重新获取锁才能继续执行
wait()的重载方法:
lock.wait(); // 无限期等待
lock.wait(1000); // 等待指定时间(毫秒)
lock.wait(1000, 500000); // 等待指定时间(毫秒+纳秒)
Object.notify()方法
notify()方法唤醒在此对象监视器上等待的单个线程。
private boolean condition = false;
// 等待条件
synchronized (lock) {
while (!condition) { // 使用while,防止虚假唤醒
lock.wait();
}
// 条件满足,执行任务
}
// 设置条件
synchronized (lock) {
condition = true;
lock.notify(); // 唤醒一个等待线程
}
notify()的要点:
- 必须在synchronized块中调用
- 只能唤醒在此对象上wait()的线程
- 如果有多个线程在等待,随机唤醒一个
- 被唤醒的线程需要重新获取锁
Object.notifyAll()方法
notifyAll()方法唤醒在此对象监视器上等待的所有线程。
// 唤醒所有等待的线程
synchronized (lock) {
lock.notifyAll(); // 唤醒所有等待的线程
}
notify() vs notifyAll():
| 特性 | notify() | notifyAll() |
|---|---|---|
| 唤醒数量 | 1个线程 | 所有等待线程 |
| 适用场景 | 所有等待线程处理相同的任务 | 等待线程处理不同的任务 |
| 性能 | 更好(只唤醒一个) | 较差(唤醒所有) |
使用注意事项
1. 必须在synchronized块中调用
// ❌ 错误:会抛出IllegalMonitorStateException
lock.wait();
// ✅ 正确
synchronized (lock) {
lock.wait();
}
2. 使用while循环检查条件(防止虚假唤醒)
// ❌ 错误:可能产生虚假唤醒
synchronized (lock) {
if (!condition) {
lock.wait();
}
}
// ✅ 正确:使用while循环
synchronized (lock) {
while (!condition) {
lock.wait();
}
}
3. 注意锁的持有时间
// ❌ 错误:持有锁时间过长
synchronized (lock) {
heavyOperation(); // 耗时操作
lock.wait();
}
// ✅ 正确:在锁外执行耗时操作
synchronized (lock) {
while (!condition) {
lock.wait();
}
}
heavyOperation(); // 在锁外执行
虚假唤醒问题
虚假唤醒: 线程可能在没有调用notify()的情况下被唤醒。
原因: 操作系统层面的信号、其他系统调用、JVM实现细节
解决方案:使用while循环而不是if
private boolean condition = false;
synchronized (lock) {
while (!condition) { // 使用while,被唤醒后再次检查
lock.wait();
}
// 条件满足,执行任务
}
10.2 Condition机制
Condition.await()
Condition提供了更灵活的等待/通知机制。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待
lock.lock();
try {
condition.await(); // 释放锁并等待
} finally {
lock.unlock();
}
// 唤醒
lock.lock();
try {
condition.signal(); // 唤醒一个等待线程
} finally {
lock.unlock();
}
await()的重载方法:
condition.await(); // 无限期等待
condition.awaitNanos(1000000); // 等待指定纳秒
condition.await(1, TimeUnit.SECONDS); // 等待指定时间
condition.awaitUntil(new Date()); // 等待到指定日期
condition.awaitUninterruptibly(); // 不响应中断
Condition.signal()
signal()唤醒一个等待的线程。
lock.lock();
try {
condition.signal(); // 唤醒一个等待线程
} finally {
lock.unlock();
}
Condition.signalAll()
signalAll()唤醒所有等待的线程。
lock.lock();
try {
condition.signalAll(); // 唤醒所有等待线程
} finally {
lock.unlock();
}
多个Condition的使用
Condition的优势:可以为不同的条件创建不同的Condition。
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // 不空条件
Condition notFull = lock.newCondition(); // 不满条件
// 生产者:等待不满条件,通知不空条件
lock.lock();
try {
while (count == items.length)
notFull.await();
// 生产...
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
// 消费者:等待不空条件,通知不满条件
lock.lock();
try {
while (count == 0)
notEmpty.await();
// 消费...
notFull.signal(); // 通知生产者
} finally {
lock.unlock();
}
优势:
- 更精确的线程唤醒控制
- 避免不必要的唤醒
- 提高性能
10.3 管道通信
PipedInputStream/PipedOutputStream
管道用于线程间的字节流通信。
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
pis.connect(pos); // 连接输入输出流
// 生产者线程
new Thread(() -> {
pos.write("数据".getBytes());
pos.close();
}).start();
// 消费者线程
new Thread(() -> {
int data;
while ((data = pis.read()) != -1) {
System.out.print((char) data);
}
pis.close();
}).start();
PipedReader/PipedWriter
管道用于线程间的字符流通信。
PipedReader pr = new PipedReader();
PipedWriter pw = new PipedWriter();
pr.connect(pw); // 连接读写器
// 生产者线程
new Thread(() -> {
pw.write("消息");
pw.close();
}).start();
// 消费者线程
new Thread(() -> {
int data;
while ((data = pr.read()) != -1) {
System.out.print((char) data);
}
pr.close();
}).start();
注意:
- 管道是阻塞的,如果缓冲区满,写操作会阻塞
- 如果缓冲区空,读操作会阻塞
- 适合一对一的线程通信
10.4 线程间数据共享
ThreadLocal
ThreadLocal为每个线程提供独立的变量副本。
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 每个线程都有自己独立的副本
int value = threadLocal.get();
threadLocal.set(value + 1);
threadLocal.remove(); // 使用完后记得移除,防止内存泄漏
ThreadLocal的应用场景:
- 用户上下文信息(用户ID、权限等)
- 数据库连接
- 日期格式化器
- 避免参数传递
InheritableThreadLocal
InheritableThreadLocal允许子线程继承父线程的ThreadLocal值。
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("父线程的值");
Thread child = new Thread(() -> {
// 子线程可以访问父线程的值
System.out.println(threadLocal.get());
});
child.start();
线程间数据传递
方式1:通过构造方法传递
new Thread(() -> {
String data = "数据1";
// 处理数据
}).start();
方式2:通过共享变量传递
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
new Thread(() -> queue.put("数据")).start();
new Thread(() -> queue.take()).start();
方式3:通过回调函数传递
new Thread(() -> {
String result = processData();
callback.onComplete(result);
}).start();