Java并发编程基础与进阶(线程·锁·原子类·通信)

13 阅读1小时+

第一章 线程基础

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()只能调用一次,重复调用会抛出IllegalThreadStateException
  • start()会创建新的线程,而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 = 1
  • Thread.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++不是原子操作,实际包含三个步骤:

  1. 读取count的值
  2. 将count加1
  3. 将新值写回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种内存操作:

  1. lock(锁定):作用于主内存,把变量标识为线程独占
  2. unlock(解锁):作用于主内存,释放锁定状态的变量
  3. read(读取):作用于主内存,把变量值从主内存传输到线程工作内存
  4. load(载入):作用于工作内存,把read得到的值放入工作内存的变量副本
  5. use(使用):作用于工作内存,把工作内存变量值传递给执行引擎
  6. assign(赋值):作用于工作内存,把执行引擎接收的值赋给工作内存变量
  7. store(存储):作用于工作内存,把工作内存变量值传送到主内存
  8. 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
        }
    }
}

可见性问题产生的原因:

  1. CPU缓存:每个CPU核心有自己的缓存,变量可能只更新在缓存中
  2. 指令重排序:编译器和CPU可能重排序指令
  3. 寄存器优化:变量可能被优化到寄存器中

保证可见性的方法:

  • 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方法,状态不可变
}

实现不可变对象的原则:

  1. 所有字段都是final
  2. 类声明为final,防止被继承
  3. 不提供修改状态的方法
  4. 如果字段是引用类型,确保引用的对象也是不可变的

Java中的不可变类:

  • String
  • IntegerLong等包装类
  • BigIntegerBigDecimal

线程封闭

**线程封闭(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互斥
    }
}

互斥关系总结:

方法使用的锁method1method2method3
method1lock1✅ 互斥❌ 不互斥✅ 互斥
method2lock2❌ 不互斥✅ 互斥❌ 不互斥
method3lock1✅ 互斥❌ 不互斥✅ 互斥

注意事项:

  • ⚠️ 锁对象不能是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消耗和性能,智能优化

偏向锁的撤销

什么时候撤销偏向锁?

撤销场景:

  1. 有其他线程竞争:发现线程ID不是当前线程
  2. 调用hashCode():偏向锁的Mark Word没有空间存储hashCode
  3. 调用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之后:

  • ✅ 引入了锁升级机制(偏向锁 → 轻量级锁 → 重量级锁)
  • ✅ 性能大幅提升
  • ✅ 在低竞争情况下,性能接近无锁

性能对比:

场景synchronizedReentrantLock
低竞争✅ 性能很好(锁升级优化)✅ 性能也很好
高竞争⚠️ 可能升级为重量级锁✅ 性能可能更好
可中断❌ 不支持✅ 支持
公平锁❌ 非公平✅ 可选公平/非公平

建议:

  • 大多数情况下,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前缀的作用:

  1. 锁定总线或缓存行

    • 在多核CPU中,确保只有一个CPU能执行这个指令
    • 保证操作的原子性
  2. 刷新缓存

    • 将缓存中的数据写回主内存
    • 使其他CPU的缓存失效
    • 保证可见性

简单理解:

  • lock前缀就像给操作加了一把"锁"
  • 确保操作是原子的、可见的
  • CPU硬件层面的保证

MESI缓存一致性协议

什么是MESI?

MESI是多核CPU的缓存一致性协议,用于保证多核CPU之间缓存的一致性。

MESI的四种状态:

状态全称含义
MModified缓存行被修改,与主内存不一致
EExclusive缓存行独占,与主内存一致
SShared缓存行共享,与主内存一致
IInvalid缓存行无效,需要从主内存加载

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: 可能变慢(升级为重量级锁)

使用场景对比

功能对比表:

特性volatilesynchronized
可见性✅ 保证✅ 保证
原子性❌ 不保证✅ 保证
有序性✅ 保证✅ 保证
互斥性❌ 不保证✅ 保证
性能✅ 较高⚠️ 中等
使用复杂度✅ 简单⚠️ 中等

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语言层面的特性,而是硬件层面的支持。

关键点:

  1. CPU指令级别:CAS是CPU的一条指令,一条指令的执行是不可分割的
  2. 不会被中断:在执行CAS指令期间,不会被其他线程或操作中断
  3. 硬件保证:这是硬件层面的保证,比软件层面的锁更底层、更高效

原子性的重要性:

// ❌ 非原子操作(不安全,有竞态条件)
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指令
        ...
    );
}

关键点解析:

  1. lock前缀

    • 锁定CPU总线或缓存行
    • 确保只有一个CPU核心能执行这个指令
    • 保证原子性
  2. cmpxchgl指令

    • x86架构的比较并交换指令
    • 一条指令完成比较和交换
    • 硬件级别的原子操作
  3. 内存屏障

    • 保证可见性(其他CPU能看到更新)
    • 防止指令重排序

简单理解:

  • Java代码 → JVM → CPU指令(lock cmpxchgl)
  • 一条CPU指令完成,不会被中断
  • 硬件保证原子性,非常高效

5.3 CAS的优缺点

优点:无锁、高性能

无锁的优势:

  1. 避免线程阻塞和唤醒的开销

    • synchronized会让线程进入阻塞状态,需要操作系统唤醒
    • CAS失败后只是自旋重试,线程不会阻塞
    • 减少了上下文切换的开销
  2. 避免死锁

    • CAS不需要获取锁,不会出现死锁问题
    • 非常适合在高并发场景使用
  3. 适合低竞争场景

    • 当多个线程竞争不激烈时,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));
}

性能对比总结:

场景CASsynchronized
低竞争✅ 性能很好(大多数成功,很少重试)⚠️ 性能稍差(有锁开销)
高竞争⚠️ 性能下降(大量自旋重试,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满载但效率低

解决方案:

  1. 限制自旋次数:超过一定次数后放弃
  2. 自适应自旋:根据历史成功率动态调整自旋次数
  3. 自旋失败后阻塞:自旋一段时间后如果还失败,就阻塞等待

性能建议:

  • 低竞争场景:使用CAS(自旋开销小)
  • 高竞争场景:考虑使用锁(阻塞等待,节省CPU)
只能保证一个变量的原子性

问题描述:

CAS只能原子地更新一个变量。如果需要对多个变量进行原子操作,CAS无法直接保证。

示例:

AtomicInteger count1 = new AtomicInteger(0);
AtomicInteger count2 = new AtomicInteger(0);

// ❌ 这两个操作不是原子的
count1.incrementAndGet();  // 操作1
count2.incrementAndGet();  // 操作2
// 问题:两个操作之间可能被其他线程打断

为什么这是个问题?

  • 如果需要保证count1和count2同时更新,CAS无法做到
  • 两个独立的CAS操作之间没有原子性保证
  • 可能导致数据不一致

解决方案:

  1. 使用synchronized:将多个操作放在同步块中
  2. 使用锁:ReentrantLock等
  3. 合并变量:将多个变量合并为一个对象,CAS更新整个对象
  4. 使用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);
});

对比总结:

特性AtomicStampedReferenceAtomicMarkableReference
标记类型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的核心数据结构

主要组成:

  1. state(同步状态)

    private volatile int state; // volatile保证可见性
    
  2. 等待队列(CLH队列)

    // 队列头节点(虚拟节点)
    private transient volatile Node head;
    // 队列尾节点
    private transient volatile Node tail;
    
  3. 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;  // 获取失败
}

简单理解:

  1. 检查锁是否空闲(state == 0)
  2. 如果空闲,CAS尝试获取锁
  3. 如果当前线程已持有锁,支持可重入
  4. 返回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;
    }
}

简单理解:

  1. 检查是否是持有锁的线程(防止非法释放)
  2. state减1
  3. 如果state变为0,完全释放锁
  4. 返回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;  // 返回剩余数量
}

简单理解:

  1. state表示可用资源数量(如Semaphore的许可证数)
  2. 尝试获取指定数量的资源
  3. 如果资源足够,CAS更新state
  4. 返回剩余资源数量
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失败,自旋重试
    }
}

简单理解:

  1. 释放指定数量的资源
  2. state增加
  3. 使用CAS更新,失败则自旋重试
  4. 返回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();           // 获取条件对象
}

方法说明:

  1. lock() - 最常用的方法,获取锁,如果锁被占用则阻塞等待
  2. lockInterruptibly() - 可中断的获取锁,线程在等待时可以被中断
  3. tryLock() - 尝试获取锁,立即返回true/false,不会阻塞
  4. tryLock(time, unit) - 在指定时间内尝试获取锁,超时返回false
  5. unlock() - 释放锁,必须在finally块中调用
  6. newCondition() - 创建Condition对象,用于线程间的协调

Lock vs synchronized

特性synchronizedLock
获取锁方式自动获取和释放手动获取和释放
可中断❌ 不可中断✅ 可中断(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();
}

执行流程(简单理解):

  1. 当前线程加入条件等待队列
  2. 释放锁
  3. 阻塞等待
  4. 被signal唤醒后,重新获取锁
  5. 继续执行

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/notifyCondition
前置条件必须在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;
    }
    // 可重入处理...
}

流程总结:

  1. 检查锁是否空闲
  2. 关键:检查队列中是否有等待的线程
  3. 如果有等待的,不插队,返回false,加入队列等待
  4. 如果没有等待的,CAS获取锁

非公平锁的获取流程

核心区别:新线程可能插队

// 非公平锁:先尝试直接获取,失败才排队
lock() {
    if (CAS直接尝试获取锁) {  // 新线程可能插队
        return;  // 成功
    }
    acquire(1);  // 失败才调用AQS的acquire
}

tryAcquire(1) {
    if (锁空闲) {
        CAS获取锁;  // 不检查队列,直接尝试
        return true/false;
    }
    // 可重入处理...
}

流程总结:

  1. 关键:先直接CAS尝试获取锁(可能插队)
  2. 失败才调用acquire,进入队列
  3. 在队列中也直接尝试获取,不检查是否轮到

锁的释放流程

释放流程(公平锁和非公平锁相同):

unlock() {
    release(1);  // 调用AQS的release
}

release(1) {
    if (tryRelease(1)) {  // 尝试释放锁
        if (队列中有等待的线程) {
            唤醒下一个等待的线程;  // 让等待的线程有机会获取锁
        }
        return true;
    }
    return false;  // 还有重入次数,未完全释放
}

流程总结:

  1. state减1
  2. 如果state变为0,完全释放锁
  3. 唤醒队列中等待的下一个线程
  4. 让等待的线程有机会获取锁

第八章 读写锁(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引入的新锁,提供了三种模式:

  1. 写锁(Writing):独占锁,类似ReentrantReadWriteLock的写锁
  2. 悲观读锁(Reading):共享锁,类似ReentrantReadWriteLock的读锁
  3. 乐观读锁(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);
    }
}

乐观读的流程:

  1. tryOptimisticRead():获取乐观读戳记,不阻塞
  2. 读取数据
  3. 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();