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

第一章 线程基础

1.1 进程与线程的概念

进程的定义和特点

**进程(Process)**是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间,包括代码段、数据段、堆栈段等。

进程的特点:

  • 独立性:进程之间相互独立,一个进程的崩溃不会影响其他进程
  • 资源隔离:每个进程拥有独立的地址空间,进程间不能直接访问对方的内存
  • 开销大:进程的创建、切换、销毁都需要较大的系统开销
  • 通信复杂:进程间通信需要通过IPC(Inter-Process Communication)机制,如管道、信号、共享内存等

线程的定义和特点

**线程(Thread)**是CPU调度的基本单位,是进程内的一个执行流。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。

线程的特点:

  • 轻量级:线程的创建、切换、销毁开销比进程小得多
  • 共享资源:同一进程内的线程共享进程的内存空间、文件描述符等资源
  • 并发执行:多个线程可以并发执行,提高程序的执行效率
  • 通信简单:线程间可以直接通过共享内存进行通信,但需要同步机制保证线程安全

进程与线程的区别

对比项 进程 线程
资源拥有 拥有独立的地址空间和资源 共享进程的地址空间和资源
创建开销 大(需要分配独立的内存空间) 小(共享进程资源)
切换开销 大(需要切换地址空间) 小(只需切换上下文)
通信方式 需要IPC机制(管道、信号等) 可直接通过共享内存通信
独立性 完全独立,一个进程崩溃不影响其他进程 相互影响,一个线程崩溃可能导致整个进程崩溃
数量 系统资源有限,进程数量较少 一个进程可以创建大量线程

多进程 vs 多线程

多进程的优势:

  • 更好的隔离性,一个进程的崩溃不会影响其他进程
  • 可以利用多核CPU,实现真正的并行
  • 适合需要高稳定性的场景

多线程的优势:

  • 创建和切换开销小,性能更高
  • 线程间通信简单,数据共享方便
  • 适合需要频繁通信和协作的场景

选择建议:

  • 需要高隔离性、高稳定性 → 选择多进程
  • 需要频繁通信、共享数据 → 选择多线程
  • 现代应用通常采用多线程 + 进程隔离的混合模式

1.2 Java线程的创建方式

方式一:继承Thread类

java 复制代码
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接口(推荐)

java 复制代码
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接口

java 复制代码
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使用
  • 适合需要返回结果的异步任务

方式四:使用线程池创建

java 复制代码
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表达式创建

java 复制代码
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(新建)

线程被创建但尚未启动的状态。

java 复制代码
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // NEW

RUNNABLE(可运行)

线程正在JVM中执行,但可能正在等待操作系统分配CPU时间片。

java 复制代码
Thread thread = new Thread(() -> {
    while(true) {
        // 运行中或等待CPU时间片
    }
});
thread.start();
System.out.println(thread.getState()); // RUNNABLE

注意: RUNNABLE状态包括:

  • Running:正在执行
  • Ready:就绪,等待CPU调度

BLOCKED(阻塞)

线程被阻塞,等待获取监视器锁(monitor lock)。

java 复制代码
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(等待)

线程无限期等待另一个线程执行特定操作。

java 复制代码
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(超时等待)

线程在指定时间内等待。

java 复制代码
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(终止)

线程执行完成或异常终止。

java 复制代码
Thread thread = new Thread(() -> {
    System.out.println("执行完成");
});
thread.start();
thread.join(); // 等待线程执行完成
System.out.println(thread.getState()); // TERMINATED

状态转换图

scss 复制代码
    NEW
     |
     | start()
     ↓
RUNNABLE ←──────────┐
     |               |
     | wait()        | notify()/notifyAll()
     ↓               |
  WAITING ──────────┘
     |
     | sleep(timeout)/wait(timeout)/join(timeout)
     ↓
TIMED_WAITING
     |
     | 获取锁失败
     ↓
  BLOCKED
     |
     | 获取到锁
     ↓
RUNNABLE
     |
     | run()方法执行完成或异常
     ↓
 TERMINATED

1.4 线程的基本操作

start()方法

启动线程,使线程进入RUNNABLE状态。

java 复制代码
Thread thread = new Thread(() -> {
    System.out.println("线程执行");
});
thread.start(); // 启动线程
// thread.start(); // 错误!不能重复调用start()

注意:

  • start()只能调用一次,重复调用会抛出IllegalThreadStateException
  • start()会创建新的线程,而run()只是普通方法调用

run()方法

线程的执行体,包含线程要执行的代码。

java 复制代码
Thread thread = new Thread(() -> {
    System.out.println("run方法执行");
});
thread.run(); // 直接调用run(),不会创建新线程,在当前线程执行
thread.start(); // 调用start(),创建新线程执行run()方法

start() vs run():

  • start():创建新线程,异步执行
  • run():普通方法调用,同步执行

sleep()方法

让当前线程休眠指定时间,进入TIMED_WAITING状态。

java 复制代码
try {
    Thread.sleep(1000); // 休眠1秒
    System.out.println("休眠结束");
} catch (InterruptedException e) {
    e.printStackTrace();
}

特点:

  • 不释放锁
  • 可能抛出InterruptedException
  • 时间到了自动唤醒

yield()方法

提示调度器当前线程愿意让出CPU时间片,但调度器可以忽略这个提示。

java 复制代码
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()方法

等待目标线程执行完成。

java 复制代码
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()方法

中断线程,设置线程的中断标志位。

java 复制代码
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()方法

检查线程的中断标志位,不会清除标志位。

java 复制代码
Thread thread = new Thread(() -> {});
thread.start();
thread.interrupt();
System.out.println(thread.isInterrupted()); // true
System.out.println(thread.isInterrupted()); // true(标志位还在)

interrupted()方法

检查当前线程的中断标志位,会清除标志位。

java 复制代码
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted()); // true
System.out.println(Thread.interrupted()); // false(标志位被清除)

setDaemon()守护线程

设置线程为守护线程,当所有非守护线程结束时,JVM会自动退出。

java 复制代码
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),数字越大优先级越高。

java 复制代码
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)

竞态条件是指程序的执行结果依赖于线程执行的相对时序。

java 复制代码
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)

数据竞争是指多个线程在没有同步的情况下,同时访问同一个共享变量,并且至少有一个线程在写。

java 复制代码
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
        }
    }
}

可见性问题

可见性是指一个线程对共享变量的修改,能够及时被其他线程看到。

java 复制代码
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可能重排序指令

解决方案:

java 复制代码
private volatile boolean running = true; // 使用volatile保证可见性

原子性问题

原子性是指一个操作要么全部执行,要么都不执行,不会被打断。

java 复制代码
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块内的操作

有序性问题

有序性是指程序执行的顺序按照代码的先后顺序执行。

java 复制代码
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):

  • 每个线程都有自己的工作内存
  • 工作内存保存了该线程使用到的变量的主内存副本
  • 线程对变量的所有操作都必须在工作内存中进行
  • 不同线程之间无法直接访问对方的工作内存

内存交互流程:

arduino 复制代码
主内存
  ↓ 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传送来的值放入主内存变量

内存可见性

内存可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。

java 复制代码
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:程序顺序规则

java 复制代码
int a = 1;  // 操作1
int b = 2;  // 操作2
// 操作1 happens-before 操作2

规则2:volatile规则

java 复制代码
volatile int x = 0;

// 线程1
x = 1; // 写操作

// 线程2
int r = x; // 读操作,能看到x=1

规则3:传递性规则

java 复制代码
// 如果 A happens-before B,B happens-before C
// 那么 A happens-before C

规则4:监视器锁规则

java 复制代码
synchronized (lock) {
    // 解锁 happens-before 后续的加锁
}

规则5:start()规则

java 复制代码
Thread t = new Thread(() -> {
    // 线程t中的操作
});
t.start(); // start() happens-before 线程t中的任何操作

规则6:join()规则

java 复制代码
Thread t = new Thread(() -> {
    // 线程t中的操作
});
t.start();
t.join(); // 线程t中的所有操作 happens-before join()返回

规则7:线程中断规则

java 复制代码
thread.interrupt(); // happens-before 检测到中断

规则8:对象终结规则

java 复制代码
// 对象的构造函数 happens-before finalize()方法

as-if-serial语义

as-if-serial语义是指:不管怎么重排序,单线程程序的执行结果不能被改变。

java 复制代码
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)**是指对象创建后状态不能被修改。

java 复制代码
// 不可变类示例
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:栈封闭

java 复制代码
public void method() {
    int localVar = 0; // 局部变量,线程安全
    localVar++;
}

方式2:ThreadLocal

java 复制代码
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

java 复制代码
public class SynchronizedExample {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
}

方式2:Lock

java 复制代码
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:原子类

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();
    }
}

无锁编程

**无锁编程(Lock-Free Programming)**使用CAS操作实现线程安全。

java 复制代码
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关键字

java 复制代码
public class Counter {
    private int count = 0;
    
    // 锁的是当前实例对象(this)
    public synchronized void increment() {
        count++;
    }
    
    // 等价于:
    public void increment2() {
        synchronized(this) {  // 等价写法
            count++;
        }
    }
}

特点说明:

  • 锁对象:当前实例对象(this)
  • 作用范围:整个方法
  • 互斥关系
    • ✅ 同一个实例的多个线程会互斥
    • ❌ 不同实例之间不互斥(各自独立)

示例理解:

java 复制代码
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关键字

java 复制代码
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对象只有一个)
    • ✅ 不同实例的线程也会互斥
    • ❌ 静态方法和实例方法的锁不同,不会互斥

示例理解:

java 复制代码
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(对象)

java 复制代码
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锁的是对象,不是代码
  • 多个线程使用同一个对象作为锁 → 互斥
  • 多个线程使用不同对象作为锁 → 不互斥
java 复制代码
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或业务对象

锁的粒度

什么是锁的粒度?

  • 粒度:锁住的范围大小
  • 粗粒度:锁住的范围大(如整个方法)
  • 细粒度:锁住的范围小(如几行代码)

原则:尽量使用细粒度锁,提高并发性能

粗粒度锁(不推荐)

问题: 锁住了不需要锁的代码,导致不必要的互斥

java 复制代码
// ❌ 粗粒度锁:锁住整个方法
private int count1 = 0;
private int count2 = 0;

public synchronized void increment1() {
    count1++;
    // 其他不相关的操作...
    // 整个方法都被锁住,影响性能
}

public synchronized void increment2() {
    count2++;  
    // 与increment1互斥,但实际上没必要
    // 因为count1和count2是不同的变量
}

问题分析:

  • count1和count2是不同的变量,互不干扰
  • 但使用synchronized方法,导致它们互斥
  • 降低了并发性能
细粒度锁(推荐)

优点: 只锁住必要的代码,提高并发性

java 复制代码
// ✅ 细粒度锁:只锁住必要的代码
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对象在内存中都有对象头,对象头中包含了锁的信息。

css 复制代码
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结构(简化理解):

css 复制代码
Mark Word (64位)
├── 锁状态(2位):标识当前锁的类型
└── 其他数据(62位):根据锁状态不同,存储不同内容

锁状态分类:
┌──────────┬──────────────────────────────────────┐
│ 锁状态   │ Mark Word内容                        │
├──────────┼──────────────────────────────────────┤
│ 无锁     │ 对象的hashCode + 分代年龄 + 状态位01  │
│ 偏向锁   │ 线程ID + Epoch + 分代年龄 + 状态位01  │
│ 轻量级锁 │ 指向栈中锁记录的指针 + 状态位00        │
│ 重量级锁 │ 指向monitor对象的指针 + 状态位10       │
│ GC标记   │ 空 + 状态位11                        │
└──────────┴──────────────────────────────────────┘

简单理解:

  • 无锁:正常对象,没有线程竞争
  • 偏向锁:只有一个线程使用,记录线程ID
  • 轻量级锁:有竞争但不激烈,使用CAS和自旋
  • 重量级锁:竞争激烈,使用操作系统级别的锁

锁的升级过程

锁升级的目的: 根据竞争情况动态调整锁策略,在保证线程安全的前提下,尽可能提高性能。

升级路径:

markdown 复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
      (单向升级,不能降级)
无锁状态

特点: 对象刚创建时,没有任何线程使用,处于无锁状态

java 复制代码
Object obj = new Object();
// 此时obj的Mark Word处于无锁状态
// 锁标志位:01,没有偏向位

使用场景:

  • 对象刚创建
  • 没有线程访问同步代码块
  • 正常的对象状态
偏向锁(Biased Locking)

设计目的: 优化单线程重复获取锁的场景

适用场景:

  • 大多数情况下,只有一个线程使用锁
  • 同一个线程多次获取同一个锁
  • 没有真正的竞争

工作原理(简单理解):

第一次获取锁:

markdown 复制代码
1. 线程1第一次进入synchronized块
2. 检查Mark Word:是否为可偏向状态?
3. 是:在Mark Word中记录线程1的ID
4. 将锁状态设置为偏向锁
5. 之后线程1再进入时,直接检查线程ID,相同就直接执行

再次获取锁(同一线程):

markdown 复制代码
1. 线程1再次进入synchronized块
2. 检查Mark Word中的线程ID:是否是自己?
3. 是:直接执行,无需任何同步操作(很快!)
4. 就像"免检通道",无需排队

代码示例(简化理解):

java 复制代码
public class Counter {
    public synchronized void increment() {
        // 第一次:升级为偏向锁,记录线程ID
        // 之后同一线程:直接执行,几乎无开销
    }
}

// 场景:单线程场景
Counter c = new Counter();
// 线程A多次调用,都很快(偏向锁优化)
for (int i = 0; i < 1000; i++) {
    c.increment();  // 第二次开始就很快了
}

偏向锁的优势:

  • 性能极好:同一线程再次获取锁几乎无开销
  • 适合单线程场景:大多数情况下就是单线程使用
  • 减少同步开销:避免CAS操作

偏向锁的获取流程:

markdown 复制代码
1. 检查Mark Word的锁标志位(是否为01,可偏向)
   ├─ 是 → 继续
   └─ 否 → 已有其他锁状态,跳过偏向锁

2. 检查线程ID是否指向当前线程
   ├─ 是 → 直接进入同步代码块(最快路径)
   └─ 否 → 尝试CAS替换线程ID
       ├─ 成功 → 获得偏向锁
       └─ 失败 → 撤销偏向锁,升级为轻量级锁
轻量级锁(Lightweight Locking)

设计目的: 当有多个线程竞争,但竞争不激烈时,使用CAS自旋代替阻塞

适用场景:

  • 有多个线程竞争锁
  • 但竞争不激烈(大部分CAS能成功)
  • 等待时间短

工作原理(简化理解):

获取锁的过程:

markdown 复制代码
1. 在栈中创建锁记录(Lock Record)
2. 复制对象头的Mark Word到锁记录(备份)
3. CAS尝试将对象头的Mark Word替换为锁记录的指针
   ├─ 成功 → 获得轻量级锁(很快,使用CAS)
   └─ 失败 → 有其他线程在竞争,自旋重试
       ├─ 自旋成功 → 获得锁
       └─ 自旋失败(自旋次数过多)→ 升级为重量级锁

为什么叫"轻量级"?

  • 不需要操作系统介入(重量级锁需要)
  • 使用CAS自旋,线程不阻塞
  • 开销比重量级锁小

代码示例:

java 复制代码
public class Counter {
    public synchronized void increment() {
        // 多线程竞争,但竞争不激烈
        // 使用轻量级锁:CAS + 自旋
        count++;
    }
}

// 场景:多个线程,但竞争不激烈
Counter c = new Counter();
// 多个线程同时调用,但大多数情况下CAS能成功
// 只有少数情况需要自旋重试

轻量级锁的特点:

  • 使用CAS:无锁编程思想,性能好
  • 自旋重试:失败后自旋几次,避免立即阻塞
  • ⚠️ CPU消耗:自旋会占用CPU,不适合高竞争场景
  • ⚠️ 可能升级:自旋失败后升级为重量级锁
重量级锁(Heavyweight Locking)

设计目的: 当竞争激烈时,使用操作系统级别的互斥量,让线程阻塞等待

适用场景:

  • 锁竞争激烈(很多线程同时竞争)
  • 轻量级锁自旋失败(自旋次数超过阈值)
  • 等待时间可能很长

工作原理(简化理解):

升级过程:

markdown 复制代码
1. 轻量级锁自旋失败(多次CAS失败)
2. 锁升级为重量级锁
3. 对象头的Mark Word指向monitor对象(管程)
4. 竞争失败的线程进入阻塞队列
5. 由操作系统进行线程调度和唤醒

为什么叫"重量级"?

  • 需要操作系统介入(操作系统级别的mutex)
  • 线程会阻塞,需要上下文切换
  • 开销比轻量级锁大

代码示例:

java 复制代码
public class Counter {
    public synchronized void increment() {
        // 很多线程同时竞争
        // 使用重量级锁:线程阻塞等待
        count++;
    }
}

// 场景:高竞争
Counter c = new Counter();
// 100个线程同时竞争,轻量级锁自旋失败
// 升级为重量级锁,大部分线程阻塞等待

重量级锁的特点:

  • 适合高竞争:竞争激烈时性能稳定
  • 节省CPU:阻塞的线程不占用CPU
  • 开销大:需要操作系统介入,上下文切换开销
  • 响应慢:线程阻塞后需要等待被唤醒

锁升级的条件和时机

升级路径(单向,不能降级):

markdown 复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
      (一旦升级,不能降级)

升级条件详解:

1. 无锁 → 偏向锁

触发条件:

  • 对象被第一个线程访问同步代码块
  • JVM启用偏向锁(JDK 15+默认禁用,但了解原理很重要)

时机:

java 复制代码
Object obj = new Object();  // 无锁状态

// 第一次访问
synchronized(obj) {
    // 升级为偏向锁,记录当前线程ID
}
2. 偏向锁 → 轻量级锁

触发条件:

  • 有其他线程尝试获取偏向锁(发现线程ID不是自己)
  • 偏向锁撤销,升级为轻量级锁

时机:

java 复制代码
// 线程1获得偏向锁
synchronized(obj) {
    // 偏向锁状态
}

// 线程2尝试获取锁(发现线程ID不是自己)
// 触发偏向锁撤销,升级为轻量级锁
synchronized(obj) {
    // 轻量级锁状态,使用CAS竞争
}
3. 轻量级锁 → 重量级锁

触发条件:

  • 自旋失败(CAS失败次数超过阈值,如10次)
  • 等待的线程数超过1个
  • 自旋时间过长

时机:

java 复制代码
// 多个线程竞争,CAS频繁失败
// 自旋多次后仍然失败
synchronized(obj) {
    // 升级为重量级锁
    // 失败的线程进入阻塞队列
}

升级决策流程(简化理解):

markdown 复制代码
线程尝试获取锁
  ↓
是否有竞争?
  ├─ 无 → 偏向锁(记录线程ID)
  └─ 有 → 轻量级锁(CAS + 自旋)
       ↓
     自旋是否成功?
       ├─ 成功 → 保持轻量级锁
       └─ 失败(超过阈值)→ 重量级锁(阻塞等待)

查看锁状态(了解即可):

可以使用JOL(Java Object Layout)工具查看对象头的锁状态:

java 复制代码
// 需要添加依赖: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编译时,分析代码后发现某个锁没有必要,就自动消除它。

为什么会消除?

  • 如果对象不会"逃逸"出方法(其他线程访问不到)
  • 就没有多线程竞争,锁就没有必要

示例:

java 复制代码
// 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)

原理: 将多个连续的加锁、解锁操作合并成一个更大的锁。

为什么需要粗化?

  • 频繁加锁解锁有开销
  • 如果连续的同步块使用同一个锁,可以合并

示例:

java 复制代码
// ❌ 原始代码:多个连续的同步块
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)

撤销过程(简单理解):

markdown 复制代码
1. 暂停拥有偏向锁的线程(安全点)
2. 检查线程状态:
   ├─ 还在执行同步代码块 → 升级为轻量级锁
   └─ 已经离开同步代码块 → 恢复到无锁状态
3. 唤醒暂停的线程

为什么需要安全点?

  • 撤销时需要修改Mark Word
  • 必须在线程安全点进行(线程暂停状态)
  • 确保操作的原子性

3.4 synchronized的局限性

虽然synchronized很强大,但也有一些限制。

不可中断

问题: 线程在等待synchronized锁时,无法响应中断

示例:

java 复制代码
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是非公平的,可能导致线程饥饿

什么是非公平?

  • 新来的线程可能"插队"
  • 比等待队列中的线程先获得锁

示例:

java 复制代码
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缓存中保存变量的副本,导致一个线程的修改,其他线程看不到。

可见性问题示例:

java 复制代码
// ❌ 问题代码:没有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解决:

java 复制代码
// ✅ 解决方案:使用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的可见性保证(简单理解):

markdown 复制代码
写volatile变量:
1. 线程修改volatile变量
2. 立即刷新到主内存(不是只写缓存)
3. 使其他CPU的缓存失效

读volatile变量:
1. 线程读取volatile变量
2. 从主内存读取(不从缓存读)
3. 保证读到最新值

生活化理解:

  • 没有volatile:就像每个人有自己的笔记本,修改了但别人看不到
  • 有volatile:就像写在公告板上,所有人都能看到最新内容

禁止指令重排序

什么是指令重排序?

为了优化性能,编译器和CPU可能会重新排列指令的执行顺序。在单线程下没问题,但在多线程下可能导致问题。

重排序问题示例:

java 复制代码
// ❌ 问题代码:可能重排序
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解决:

java 复制代码
// ✅ 解决方案:使用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通过插入内存屏障来禁止重排序:

arduino 复制代码
写volatile变量:
普通写1
普通写2
─────── StoreStore屏障 ───────  ← 禁止上面的普通写和volatile写重排序
volatile写
─────── StoreLoad屏障 ───────  ← 禁止volatile写和下面的操作重排序

读volatile变量:
volatile读
─────── LoadLoad屏障 ───────  ← 禁止volatile读和下面的读重排序
─────── LoadStore屏障 ─────── ← 禁止volatile读和下面的写重排序
普通读

简单理解:

  • 内存屏障就像"栏杆",阻止指令跨越
  • volatile写前的操作不能移到写之后
  • volatile读后的操作不能移到读之前
  • 保证有序性

不保证原子性

什么是原子性?

  • 原子性:操作要么全部执行,要么都不执行,不会被打断
  • volatile只保证可见性和有序性,不保证原子性

原子性问题示例:

java 复制代码
// ❌ 错误: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++实际上包含三个步骤:

java 复制代码
// 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无法保证这三个操作作为一个整体执行

解决方案:

java 复制代码
// ✅ 方案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屏障

arduino 复制代码
Load1;        // 加载操作1
LoadLoad屏障; // 栏杆
Load2;        // 加载操作2

作用:确保Load1在Load2之前执行

2. StoreStore屏障

arduino 复制代码
Store1;        // 存储操作1
StoreStore屏障; // 栏杆
Store2;        // 存储操作2

作用:确保Store1的数据刷新到主内存,再执行Store2

3. LoadStore屏障

arduino 复制代码
Load1;        // 加载操作1
LoadStore屏障; // 栏杆
Store2;        // 存储操作2

作用:确保Load1在Store2之前执行

4. StoreLoad屏障

arduino 复制代码
Store1;        // 存储操作1
StoreLoad屏障; // 栏杆(最重,开销最大)
Load2;         // 加载操作2

作用:确保Store1的数据刷新到主内存,再执行Load2

volatile的内存屏障插入策略(简化理解):

java 复制代码
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前缀的指令。

汇编代码示例(了解即可):

assembly 复制代码
; volatile写操作
mov    %eax,0x10(%esi)    ; 将值写入内存
lock addl $0x0,(%esp)     ; lock前缀,锁定缓存行

lock前缀的作用:

  1. 锁定总线或缓存行

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

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

简单理解:

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

MESI缓存一致性协议

什么是MESI?

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

MESI的四种状态:

状态 全称 含义
M Modified 缓存行被修改,与主内存不一致
E Exclusive 缓存行独占,与主内存一致
S Shared 缓存行共享,与主内存一致
I Invalid 缓存行无效,需要从主内存加载

volatile变量的缓存一致性(简化理解):

markdown 复制代码
场景:CPU1写volatile变量

1. CPU1修改volatile变量
   → 缓存行状态变为M(Modified)

2. 通过总线发送消息
   → 通知其他CPU:这个缓存行已失效

3. 其他CPU收到消息
   → 将对应缓存行状态改为I(Invalid)

4. 其他CPU读取时
   → 发现缓存无效,从主内存重新加载
   → 保证读到最新值

为什么需要MESI?

  • 每个CPU有自己的缓存
  • 需要保证所有CPU看到的数据一致
  • MESI协议自动处理缓存一致性

4.3 volatile的使用场景

状态标志

最常用的场景: 使用volatile作为线程间的状态标志

示例:

java 复制代码
// ✅ 推荐:使用volatile作为状态标志
private volatile boolean shutdown = false;

public void shutdown() {
    shutdown = true;  // 其他线程能立即看到
}

public void doWork() {
    while (!shutdown) {
        // 执行任务
        // 能及时响应shutdown的变化
    }
}

为什么适合用volatile?

  • ✅ 只需要可见性(线程间通信)
  • ✅ 不需要原子性(只是boolean标志)
  • ✅ 简单高效(比synchronized轻量)

适用场景:

  • ✅ 线程启动/停止标志
  • ✅ 配置开关
  • ✅ 状态切换标志

双重检查锁定(DCL)

什么是DCL?

双重检查锁定是一种单例模式的实现方式,通过两次检查来减少锁的使用。

错误的单例模式:

java 复制代码
// ❌ 错误:可能有问题
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()包含三个步骤,可能被重排序:

java 复制代码
// 正常顺序
1. 分配内存空间
2. 初始化对象(调用构造函数)
3. 将引用赋值给instance

// 可能重排序为(危险!)
1. 分配内存空间
3. 将引用赋值给instance  // instance != null,但对象未初始化!
2. 初始化对象

// 问题:线程B可能拿到未完全初始化的对象

使用volatile解决:

java 复制代码
// ✅ 正确:使用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的工作原理:

csharp 复制代码
第一次检查(无锁):快速路径,大多数情况下直接返回
  ↓
  如果为null,进入同步块
  ↓
第二次检查(有锁):确保只创建一个实例
  ↓
  如果仍为null,创建实例

优势:

  • 第一次检查无锁,性能好
  • 只在第一次创建时加锁
  • 之后都是无锁访问

单例模式中的应用

枚举方式(推荐):

java 复制代码
public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // ...
    }
}

静态内部类方式:

java 复制代码
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

性能对比(简化理解):

arduino 复制代码
低竞争场景:
volatile: 很快(几乎无开销)
synchronized: 较快(偏向锁/轻量级锁)

高竞争场景:
volatile: 仍然很快(只是刷新缓存)
synchronized: 可能变慢(升级为重量级锁)

使用场景对比

功能对比表:

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

volatile适用场景:

  • 状态标志:boolean类型的线程间通信
  • 双重检查锁定:单例模式
  • 读多写少:只需要可见性,不需要互斥
  • 独立观察:发布观察结果给其他线程

synchronized适用场景:

  • 需要原子性:多步骤操作需要原子执行
  • 需要互斥:同一时刻只能有一个线程执行
  • 复杂同步:需要更复杂的同步逻辑

选择建议:

arduino 复制代码
只需要可见性?
  ├─ 是 → 使用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操作逻辑(简单理解):

markdown 复制代码
1. 读取当前值 V
2. 比较:V 是否等于预期值 A?
   - 如果相等 → 将 V 更新为 B(交换成功)
   - 如果不相等 → 不更新(交换失败,可能被别人改过了)
3. 返回操作结果

伪代码(简化版):

java 复制代码
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. 硬件保证:这是硬件层面的保证,比软件层面的锁更底层、更高效

原子性的重要性:

java 复制代码
// ❌ 非原子操作(不安全,有竞态条件)
if (value == expected) {
    value = newValue;  // 这两步不是原子的
    // 问题:在检查和赋值之间,value可能被其他线程修改
}

// ✅ CAS原子操作(安全)
compareAndSwap(value, expected, newValue);  
// 一步完成,不会被中断,原子性保证

为什么普通操作不是原子的?

  • 普通操作包含多个步骤(读取、比较、写入)
  • 在多线程环境下,这些步骤之间可能被其他线程打断
  • 导致数据不一致问题

CPU原语支持

不同CPU架构的CAS实现:

x86/x64架构(Intel/AMD):

assembly 复制代码
; CMPXCHG指令(Compare and Exchange)
CMPXCHG dest, src
; 功能:比较EAX寄存器中的值和dest,如果相等,将src写入dest
; 这是x86架构提供的原子指令

ARM架构(手机/嵌入式设备):

assembly 复制代码
; LDREX/STREX指令对(Load-Exclusive/Store-Exclusive)
LDREX R1, [R0]        ; 加载并独占访问
CMP R1, R2            ; 比较
STREXEQ R3, R4, [R0]  ; 条件存储(如果独占状态还在)
; 通过独占访问机制实现原子操作

Java中的CAS:

Java通过Unsafe类调用底层CPU指令,对开发者来说是透明的。

java 复制代码
// 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实例(仅了解,不要直接使用):

java 复制代码
// ⚠️ 注意:这只是演示,生产环境不要这样做
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方法:

java 复制代码
// 针对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:要设置的新值

实际使用(简化示例):

java 复制代码
// 实际开发中,不需要自己实现,直接使用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指令。

c 复制代码
// 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都能成功,不需要重试

性能对比代码示例:

java 复制代码
// ❌ 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只检查值是否相同,无法发现"被换过"这个事实

问题示例:

makefile 复制代码
时间线:
T1: 线程1读取值 = A
T2: 线程2修改值:A -> B
T3: 线程2修改值:B -> A(又改回来了)
T4: 线程1执行CAS(A, C)
    结果:CAS成功!但实际上值已经被修改过了

示例代码(简化版):

java 复制代码
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。

具体表现:

java 复制代码
// 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无法直接保证。

示例:

java 复制代码
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:将多个值封装在一个对象中

示例:

java 复制代码
// ✅ 方案1:使用synchronized
synchronized(this) {
    count1++;
    count2++;
}

// ✅ 方案2:合并为对象
class CountPair {
    int count1, count2;
}
AtomicReference<CountPair> pair = new AtomicReference<>();

5.4 ABA问题及解决方案

ABA问题的产生场景

典型场景:无锁栈的实现

在实现无锁数据结构(如栈、队列)时,ABA问题特别容易出现。

问题示例(无锁栈):

java 复制代码
// 简化版无锁栈
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问题的时间线:

ini 复制代码
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时同时检查值和版本号

实现思路:

java 复制代码
// 伪代码说明
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问题。

使用方式:

java 复制代码
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问题的示例:

java 复制代码
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失败,因为版本号不匹配 ✅

核心方法:

java 复制代码
// 获取值和版本号
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小)

使用方式:

java 复制代码
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      // 期望标记和新标记
);

示例:

java 复制代码
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的核心数据结构

主要组成:

  1. state(同步状态)

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

    java 复制代码
    // 队列头节点(虚拟节点)
    private transient volatile Node head;
    // 队列尾节点
    private transient volatile Node tail;
  3. Node节点

    java 复制代码
    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操作入队和出队

队列结构:

bash 复制代码
head (虚拟节点)
  ↓
Node1 ←→ Node2 ←→ Node3 (tail)
  ↑        ↑        ↑
Thread1  Thread2  Thread3

6.2 AQS的核心方法

tryAcquire/tryRelease(独占模式)

独占模式(Exclusive): 同一时刻只有一个线程能获取锁。

理解要点:

  • 独占模式就像只有一个座位的会议室
  • 其他线程必须等待当前线程释放锁
  • 典型应用:ReentrantLock
tryAcquire(尝试获取锁)

方法定义: 子类需要实现这个方法,定义如何获取锁。

java 复制代码
// AQS中的抽象方法,子类必须实现
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

ReentrantLock的实现逻辑(简化理解):

java 复制代码
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(尝试释放锁)

方法定义: 子类需要实现这个方法,定义如何释放锁。

java 复制代码
// AQS中的抽象方法,子类必须实现
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

ReentrantLock的实现逻辑(简化理解):

java 复制代码
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:获取成功,但没有剩余资源了
  • 正数:获取成功,还有剩余资源
java 复制代码
// AQS中的抽象方法
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

Semaphore的实现逻辑(简化理解):

java 复制代码
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(尝试释放共享锁)

方法定义: 子类需要实现这个方法,定义如何释放共享锁。

java 复制代码
// AQS中的抽象方法
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

Semaphore的实现逻辑(简化理解):

java 复制代码
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(独占模式获取锁)

方法作用: 独占模式下获取锁的完整流程,包括尝试获取、入队、阻塞等。

java 复制代码
public final void acquire(int arg) {
    // 步骤1:尝试获取锁
    if (!tryAcquire(arg) &&
        // 步骤2:失败则加入队列并自旋尝试
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 步骤3:如果被中断,恢复中断状态
        selfInterrupt();
}

执行流程详解:

scss 复制代码
1. tryAcquire(arg)
   ├─ 成功 → 直接返回,获取锁成功
   └─ 失败 ↓

2. addWaiter(Node.EXCLUSIVE)
   └─ 创建节点,加入等待队列

3. acquireQueued(node, arg)
   ├─ 自旋尝试获取锁
   ├─ 成功 → 返回false,获取锁成功
   └─ 失败 → 阻塞线程,等待被唤醒

4. selfInterrupt()
   └─ 如果被中断过,恢复中断状态

简单理解:

  • 先尝试快速获取锁(tryAcquire)
  • 失败则加入等待队列
  • 在队列中自旋尝试,还不行就阻塞
  • 等待其他线程释放锁后唤醒
acquireShared(共享模式获取锁)

方法作用: 共享模式下获取锁的完整流程。

java 复制代码
public final void acquireShared(int arg) {
    // tryAcquireShared返回值 < 0 表示获取失败
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);  // 失败则进入共享模式获取流程
}

简单理解:

  • 尝试获取共享资源
  • 返回值小于0表示资源不足
  • 失败则进入队列等待

release/releaseShared(释放锁的核心流程)

这些方法实现了完整的释放锁流程,包括唤醒等待线程。

release(独占模式释放锁)

方法作用: 独占模式下释放锁,并唤醒等待队列中的线程。

java 复制代码
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;  // 释放失败(还有重入次数)
}

执行流程详解:

scss 复制代码
1. tryRelease(arg)
   ├─ 成功(完全释放) → 继续下一步
   └─ 失败(还有重入) → 返回false

2. unparkSuccessor(h)
   └─ 唤醒等待队列中的下一个线程

简单理解:

  • 尝试释放锁
  • 如果完全释放,唤醒队列中的下一个线程
  • 让等待的线程有机会获取锁
releaseShared(共享模式释放锁)

方法作用: 共享模式下释放资源,并唤醒等待的线程。

java 复制代码
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的访问:

java 复制代码
// 获取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节点详解:

java 复制代码
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)
java 复制代码
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)
java 复制代码
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(自旋获取锁)
java 复制代码
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(阻塞)
java 复制代码
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 阻塞当前线程
    return Thread.interrupted(); // 返回中断状态并清除
}

6.4 基于AQS的实现类

ReentrantLock

ReentrantLock基于AQS的独占模式实现。

java 复制代码
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的共享模式和独占模式实现。

java 复制代码
public class ReentrantReadWriteLock implements ReadWriteLock {
    private final ReadLock readerLock;
    private final WriteLock writerLock;
    
    // 使用AQS的state:
    // 高16位:读锁计数
    // 低16位:写锁计数
}

Semaphore

Semaphore基于AQS的共享模式实现。

java 复制代码
public class Semaphore {
    private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // state表示可用许可证数量
        // tryAcquireShared:获取许可证
        // tryReleaseShared:释放许可证
    }
}

CountDownLatch

CountDownLatch基于AQS的共享模式实现。

java 复制代码
public class CountDownLatch {
    private static final class Sync extends AbstractQueuedSynchronizer {
        // state表示计数器值
        // countDown:state - 1
        // await:等待state == 0
    }
}

CyclicBarrier

CyclicBarrier基于ReentrantLock和Condition实现。

java 复制代码
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更灵活的锁操作。

java 复制代码
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

特性 synchronized Lock
获取锁方式 自动获取和释放 手动获取和释放
可中断 ❌ 不可中断 ✅ 可中断(lockInterruptibly)
超时获取 ❌ 不支持 ✅ 支持(tryLock)
公平锁 ❌ 非公平 ✅ 可选公平/非公平
多个条件 ❌ 只有一个条件 ✅ 可以有多个Condition
使用复杂度 简单 较复杂(需要finally释放)

使用示例对比:

java 复制代码
// 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也是可重入的

示例代码:

java 复制代码
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时,锁被完全释放

公平锁与非公平锁

核心区别: 获取锁的顺序不同

非公平锁(默认)

特点: 新来的线程可能"插队",直接尝试获取锁

java 复制代码
ReentrantLock lock = new ReentrantLock();  // 默认非公平锁
// 或
ReentrantLock lock = new ReentrantLock(false);  // 显式指定非公平锁

工作原理:

java 复制代码
// 非公平锁:先尝试直接获取锁,失败才排队
lock() {
    if (CAS尝试直接获取锁) {  // 新线程可能插队
        return;  // 成功
    }
    加入队列等待;  // 失败才排队
}

优缺点:

  • ✅ 性能更好(减少线程切换)
  • ✅ 吞吐量更高
  • ❌ 可能导致线程饥饿(某些线程一直获取不到锁)

适用场景: 大多数场景推荐使用非公平锁

公平锁

特点: 严格按照等待时间顺序获取锁,先来先服务

java 复制代码
ReentrantLock lock = new ReentrantLock(true);  // 公平锁

工作原理:

java 复制代码
// 公平锁:先检查队列,有等待的线程就排队
lock() {
    if (队列中有等待的线程) {
        加入队列等待;  // 不插队
    } else {
        尝试获取锁;
    }
}

优缺点:

  • ✅ 公平性保证,避免饥饿
  • ✅ 等待时间长的线程优先获得锁
  • ❌ 性能较差(更多上下文切换)
  • ❌ 吞吐量较低

适用场景: 需要严格公平性的场景

性能对比总结:

  • 非公平锁:性能好(约快10-20%),适合大多数场景
  • 公平锁:性能差,但更公平,适合对公平性要求高的场景
  • 选择建议:除非有特殊需求,否则使用非公平锁

7.3 Condition接口

Condition的作用

Condition提供了类似Object.wait/notify的线程等待和唤醒机制,但功能更强大。

理解要点:

  • Condition是Lock的等待/通知机制
  • 一个Lock可以创建多个Condition
  • 比wait/notify更灵活、更精确

Condition接口方法:

java 复制代码
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唤醒

java 复制代码
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)

java 复制代码
// 唤醒一个等待线程
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

java 复制代码
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内部结构(简化理解):

java 复制代码
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的方法

公平锁的获取流程

核心区别:获取锁前先检查队列

java 复制代码
// 公平锁:先检查队列,有等待的就不插队
lock() {
    acquire(1);  // 调用AQS的acquire
}

tryAcquire(1) {
    if (锁空闲) {
        if (队列中有等待的线程) {
            return false;  // 有等待的,不插队
        }
        CAS获取锁;
        return true;
    }
    // 可重入处理...
}

流程总结:

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

非公平锁的获取流程

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

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

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

流程总结:

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

锁的释放流程

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

java 复制代码
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的共享模式实现,允许多个线程同时读取。

java 复制代码
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReadLock readLock = rwLock.readLock();

// 多个线程可以同时获取读锁
readLock.lock();
try {
    // 读取数据,多个线程可以同时执行这里
} finally {
    readLock.unlock();
}

特点:

  • ✅ 多个线程可以同时持有读锁
  • ✅ 读锁是可重入的
  • ❌ 读锁会阻塞写锁的获取(有读锁时不能获取写锁)

适用场景: 读多写少的场景,如缓存、配置读取

写锁的排他性

写锁基于AQS的独占模式实现,同一时刻只有一个线程能写入。

java 复制代码
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
WriteLock writeLock = rwLock.writeLock();

// 同一时刻只有一个线程能获取写锁
writeLock.lock();
try {
    // 写入数据,同一时刻只有一个线程执行这里
} finally {
    writeLock.unlock();
}

特点:

  • ✅ 同一时刻只有一个线程能持有写锁
  • ❌ 写锁会阻塞所有读锁和写锁
  • ✅ 写锁是可重入的

适用场景: 数据写入、更新操作

锁降级(写锁→读锁)

锁降级: 将写锁降级为读锁(支持

理解要点:

  • 在持有写锁的情况下,先获取读锁,再释放写锁
  • 这样可以保证数据一致性,同时允许其他线程读取
java 复制代码
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. 释放读锁
}

注意事项:

  • ⚠️ 必须在持有写锁的情况下获取读锁
  • ⚠️ 先获取读锁,再释放写锁(顺序很重要)
  • 不能先释放写锁,再获取读锁(中间可能被其他线程获取写锁)

锁升级(读锁→写锁)

锁升级: 将读锁升级为写锁(不支持

理解要点:

  • 不能在持有读锁的情况下直接获取写锁
  • 会导致死锁:写锁等待读锁释放,但读锁不会释放
java 复制代码
// ❌ 错误:会导致死锁
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的拆分:

java 复制代码
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结构:

perl 复制代码
state (32位)
├── 高16位:读锁计数(最多65535个读锁)
└── 低16位:写锁计数(重入次数)

低16位存储写锁状态

写锁的获取:

java 复制代码
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;
}

读锁的获取和释放

读锁的获取:

java 复制代码
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);
}

读锁的释放:

java 复制代码
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方法

写锁的释放:

java 复制代码
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(特别是在读多写少的场景)

乐观读

java 复制代码
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):验证戳记是否有效
    • 有效:说明没有写操作,读取成功
    • 无效:说明有写操作,升级为悲观读锁

悲观读

java 复制代码
public double read() {
    // 获取悲观读锁
    long stamp = lock.readLock();
    try {
        return x + y;
    } finally {
        lock.unlockRead(stamp);
    }
}

悲观读锁:

  • 类似ReentrantReadWriteLock的读锁
  • 多个线程可以同时持有
  • 与写锁互斥

写锁

java 复制代码
public void move(double deltaX, double deltaY) {
    // 获取写锁
    long stamp = lock.writeLock();
    try {
        x += deltaX;
        y += deltaY;
    } finally {
        lock.unlockWrite(stamp);
    }
}

写锁:

  • 独占锁,同一时刻只有一个线程能持有
  • 与读锁和写锁都互斥

性能对比

性能测试:

java 复制代码
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. 无锁编程

java 复制代码
// 传统方式(需要锁)
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常用方法:

java 复制代码
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);        // 获取并更新

简单应用示例:

java 复制代码
// 线程安全的计数器
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet();  // 原子操作,无需锁
}

AtomicLong

AtomicLong与AtomicInteger类似,用于长整型。

java 复制代码
AtomicLong total = new AtomicLong(0L);
total.addAndGet(100);                  // 增加100
total.get();                           // 获取值

注意:

  • 在32位JVM上,long的读写不是原子的
  • AtomicLong保证long类型的原子操作
  • JDK 8+推荐使用LongAdder,性能更好

AtomicBoolean

AtomicBoolean用于布尔类型的原子操作。

java 复制代码
AtomicBoolean flag = new AtomicBoolean(false);
flag.get();                            // 获取值
flag.set(true);                        // 设置值
flag.compareAndSet(false, true);      // 比较并设置
flag.getAndSet(false);                // 获取并设置
flag.lazySet(true);                   // 延迟设置

状态标志示例:

java 复制代码
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用于原子地更新数组中的元素。

java 复制代码
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类似,用于长整型数组。

java 复制代码
AtomicLongArray array = new AtomicLongArray(10);
array.addAndGet(0, 100);               // 方法同AtomicIntegerArray

AtomicReferenceArray

AtomicReferenceArray用于引用类型数组的原子操作。

java 复制代码
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用于原子地更新引用类型变量。

java 复制代码
AtomicReference<String> ref = new AtomicReference<>("初始值");
ref.get();                              // 获取值
ref.set("新值");                        // 设置值
ref.compareAndSet("新值", "更新值");   // 比较并设置
ref.getAndSet("最终值");               // 获取并设置

单例模式应用:

java 复制代码
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问题。

java 复制代码
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标记代替版本号。

java 复制代码
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用于原子地更新对象的整型字段。

java 复制代码
// 字段必须是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用于长整型字段的原子更新。

java 复制代码
public class Account {
    private volatile long balance = 0;
}

AtomicLongFieldUpdater<Account> updater = 
    AtomicLongFieldUpdater.newUpdater(Account.class, "balance");
updater.addAndGet(account, 100);       // 方法同AtomicIntegerFieldUpdater

AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater用于引用类型字段的原子更新。

java 复制代码
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操作。

java 复制代码
// 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类直接操作内存。

java 复制代码
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失败后会自旋重试。

java 复制代码
// 自旋实现示例
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()方法使当前线程进入等待状态,直到被其他线程唤醒。

java 复制代码
private final Object lock = new Object();

// 等待
synchronized (lock) {
    lock.wait();  // 释放锁,进入等待状态
}

// 唤醒
synchronized (lock) {
    lock.notify();  // 唤醒一个等待的线程
}

wait()的要点:

  • 必须在synchronized块中调用
  • 调用wait()会释放锁
  • 线程进入WAITING状态
  • 被唤醒后需要重新获取锁才能继续执行

wait()的重载方法:

java 复制代码
lock.wait();                      // 无限期等待
lock.wait(1000);                  // 等待指定时间(毫秒)
lock.wait(1000, 500000);         // 等待指定时间(毫秒+纳秒)

Object.notify()方法

notify()方法唤醒在此对象监视器上等待的单个线程。

java 复制代码
private boolean condition = false;

// 等待条件
synchronized (lock) {
    while (!condition) {  // 使用while,防止虚假唤醒
        lock.wait();
    }
    // 条件满足,执行任务
}

// 设置条件
synchronized (lock) {
    condition = true;
    lock.notify();  // 唤醒一个等待线程
}

notify()的要点:

  • 必须在synchronized块中调用
  • 只能唤醒在此对象上wait()的线程
  • 如果有多个线程在等待,随机唤醒一个
  • 被唤醒的线程需要重新获取锁

Object.notifyAll()方法

notifyAll()方法唤醒在此对象监视器上等待的所有线程。

java 复制代码
// 唤醒所有等待的线程
synchronized (lock) {
    lock.notifyAll();  // 唤醒所有等待的线程
}

notify() vs notifyAll():

特性 notify() notifyAll()
唤醒数量 1个线程 所有等待线程
适用场景 所有等待线程处理相同的任务 等待线程处理不同的任务
性能 更好(只唤醒一个) 较差(唤醒所有)

使用注意事项

1. 必须在synchronized块中调用

java 复制代码
// ❌ 错误:会抛出IllegalMonitorStateException
lock.wait();

// ✅ 正确
synchronized (lock) {
    lock.wait();
}

2. 使用while循环检查条件(防止虚假唤醒)

java 复制代码
// ❌ 错误:可能产生虚假唤醒
synchronized (lock) {
    if (!condition) {
        lock.wait();
    }
}

// ✅ 正确:使用while循环
synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
}

3. 注意锁的持有时间

java 复制代码
// ❌ 错误:持有锁时间过长
synchronized (lock) {
    heavyOperation();  // 耗时操作
    lock.wait();
}

// ✅ 正确:在锁外执行耗时操作
synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
}
heavyOperation();  // 在锁外执行

虚假唤醒问题

虚假唤醒: 线程可能在没有调用notify()的情况下被唤醒。

原因: 操作系统层面的信号、其他系统调用、JVM实现细节

解决方案:使用while循环而不是if

java 复制代码
private boolean condition = false;

synchronized (lock) {
    while (!condition) {  // 使用while,被唤醒后再次检查
        lock.wait();
    }
    // 条件满足,执行任务
}

10.2 Condition机制

Condition.await()

Condition提供了更灵活的等待/通知机制。

java 复制代码
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()的重载方法:

java 复制代码
condition.await();                                    // 无限期等待
condition.awaitNanos(1000000);                       // 等待指定纳秒
condition.await(1, TimeUnit.SECONDS);               // 等待指定时间
condition.awaitUntil(new Date());                    // 等待到指定日期
condition.awaitUninterruptibly();                    // 不响应中断

Condition.signal()

signal()唤醒一个等待的线程。

java 复制代码
lock.lock();
try {
    condition.signal();  // 唤醒一个等待线程
} finally {
    lock.unlock();
}

Condition.signalAll()

signalAll()唤醒所有等待的线程。

java 复制代码
lock.lock();
try {
    condition.signalAll();  // 唤醒所有等待线程
} finally {
    lock.unlock();
}

多个Condition的使用

Condition的优势:可以为不同的条件创建不同的Condition。

java 复制代码
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

管道用于线程间的字节流通信。

java 复制代码
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

管道用于线程间的字符流通信。

java 复制代码
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为每个线程提供独立的变量副本。

java 复制代码
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

// 每个线程都有自己独立的副本
int value = threadLocal.get();
threadLocal.set(value + 1);
threadLocal.remove();  // 使用完后记得移除,防止内存泄漏

ThreadLocal的应用场景:

  • 用户上下文信息(用户ID、权限等)
  • 数据库连接
  • 日期格式化器
  • 避免参数传递

InheritableThreadLocal

InheritableThreadLocal允许子线程继承父线程的ThreadLocal值。

java 复制代码
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

threadLocal.set("父线程的值");

Thread child = new Thread(() -> {
    // 子线程可以访问父线程的值
    System.out.println(threadLocal.get());
});
child.start();

线程间数据传递

方式1:通过构造方法传递

java 复制代码
new Thread(() -> {
    String data = "数据1";
    // 处理数据
}).start();

方式2:通过共享变量传递

java 复制代码
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
new Thread(() -> queue.put("数据")).start();
new Thread(() -> queue.take()).start();

方式3:通过回调函数传递

java 复制代码
new Thread(() -> {
    String result = processData();
    callback.onComplete(result);
}).start();
相关推荐
祎直向前1 天前
linuxshell测试题
前端·chrome
irises1 天前
开源项目next-ai-draw-io核心能力拆解
前端·后端·llm
2509_940880221 天前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
_李小白1 天前
【Android FrameWork】延伸阅读: Android 进程管理
android
pas1361 天前
28-mini-vue customRender
前端·javascript·vue.js
fatiaozhang95271 天前
万能通刷包_非高安版_海思MV300H/MV310_原机安卓4升级安卓9_全分区烧录包支持多无线及遥控_带adb权限(2026)
android·adb·电视盒子·刷机固件·机顶盒刷机·海思安卓4升级安卓9
REDcker1 天前
web 端 H265 软解码实现原理与使用说明
前端·音视频·播放器·h265·解码·软解码
倚栏听风雨1 天前
深度拆解:从 npm install 到手写一个全局 CLI 工具
前端