跳转至

线程同步与内存模型 (Thread Synchronization & Memory Model)

深入理解synchronized、volatile关键字,掌握Java内存模型(JMM)和happens-before规则

目录


1. 为什么需要同步 (Why Synchronization?)

1.1 线程安全问题

当多个线程访问共享资源时,如果没有同步机制,可能出现:

  1. 竞态条件(Race Condition) - 多个线程同时修改共享变量
  2. 数据不一致 - 读取到中间状态的数据
  3. 可见性问题 - 一个线程的修改对另一个线程不可见

1.2 示例:线程不安全的计数器

/**
 * 线程不安全的计数器示例
 * Unsafe Counter Example
 */
public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++; // 不是原子操作!
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeCounter counter = new UnsafeCounter();

        // 创建10个线程,每个线程增加1000次
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }

        // 等待所有线程完成
        for (Thread thread : threads) {
            thread.join();
        }

        // 期望结果:10000,实际可能小于10000
        System.out.println("最终计数: " + counter.getCount());
    }
}

问题分析:

count++ 不是原子操作,实际包含3步: 1. 读取count的值 2. 将值加1 3. 写回count

多个线程可能同时读取到相同的值,导致丢失更新。

1.3 在算力平台中的应用

在算力平台的结算系统中,多个线程可能同时处理用户钱包的扣费操作,必须保证线程安全:

// 线程不安全的钱包操作
public class UnsafeWallet {
    private Long balance = 1000L;

    public void deduct(Long amount) {
        if (balance >= amount) {
            balance -= amount; // 线程不安全!
        }
    }
}

// 线程安全的钱包操作(使用synchronized)
public class SafeWallet {
    private Long balance = 1000L;

    public synchronized void deduct(Long amount) {
        if (balance >= amount) {
            balance -= amount; // 线程安全
        }
    }
}

2. synchronized关键字 (synchronized Keyword)

2.1 synchronized的作用

synchronized关键字提供了一种**互斥锁(Mutex Lock)**机制,保证同一时刻只有一个线程能执行被保护的代码块。

作用: 1. 原子性(Atomicity) - 保证操作的原子性 2. 可见性(Visibility) - 保证变量的可见性 3. 有序性(Ordering) - 保证一定的执行顺序

2.2 synchronized的用法

方式一:同步方法(Synchronized Method)

/**
 * 同步方法示例
 * Synchronized Method Example
 */
public class SynchronizedCounter {
    private int count = 0;

    // 同步实例方法:锁对象是this
    public synchronized void increment() {
        count++;
    }

    // 同步静态方法:锁对象是类对象(SynchronizedCounter.class)
    public static synchronized void staticMethod() {
        // 静态方法同步
    }

    public int getCount() {
        return count;
    }
}

锁对象: - 实例方法:锁对象是this(当前实例) - 静态方法:锁对象是类对象(Class对象)

方式二:同步代码块(Synchronized Block)

/**
 * 同步代码块示例
 * Synchronized Block Example
 */
public class SynchronizedBlock {
    private int count = 0;
    private final Object lock = new Object(); // 锁对象

    public void increment() {
        // 同步代码块:锁对象是lock
        synchronized (lock) {
            count++;
        }
    }

    // 也可以使用this作为锁对象
    public void increment2() {
        synchronized (this) {
            count++;
        }
    }

    // 使用类对象作为锁(静态同步)
    public void staticSync() {
        synchronized (SynchronizedBlock.class) {
            // 静态同步代码块
        }
    }
}

优势: 可以更细粒度地控制锁的范围,提高性能。

2.3 synchronized的可重入性(Reentrant)

/**
 * synchronized可重入性示例
 * Reentrant Synchronization Example
 */
public class ReentrantDemo {
    public synchronized void method1() {
        System.out.println("method1");
        method2(); // 调用另一个同步方法
    }

    public synchronized void method2() {
        System.out.println("method2");
    }

    public static void main(String[] args) {
        ReentrantDemo demo = new ReentrantDemo();
        demo.method1(); // 可以正常执行,不会死锁
    }
}

可重入性: 同一个线程可以多次获取同一个锁,不会死锁。

2.4 锁的释放

/**
 * 锁的释放时机
 * Lock Release Timing
 */
public class LockRelease {
    public synchronized void method1() {
        // 执行代码
        // 方法执行完毕,自动释放锁
    }

    public void method2() {
        synchronized (this) {
            // 执行代码
            // 代码块执行完毕,自动释放锁
        }
    }

    public void method3() {
        synchronized (this) {
            if (condition) {
                return; // 提前返回,锁也会自动释放
            }
            // 其他代码
        }
    }

    public void method4() {
        synchronized (this) {
            try {
                // 可能抛出异常
                riskyOperation();
            } catch (Exception e) {
                // 即使抛出异常,锁也会自动释放
            }
        }
    }
}

锁的释放时机: 1. 同步方法执行完毕 2. 同步代码块执行完毕 3. 方法提前返回(return) 4. 抛出异常

2.5 对象锁 vs 类锁

/**
 * 对象锁 vs 类锁
 * Instance Lock vs Class Lock
 */
public class LockType {
    private int instanceVar = 0;
    private static int staticVar = 0;

    // 对象锁:不同实例之间不互斥
    public synchronized void instanceMethod() {
        instanceVar++;
    }

    // 类锁:所有实例共享,互斥
    public static synchronized void staticMethod() {
        staticVar++;
    }

    public static void main(String[] args) {
        LockType obj1 = new LockType();
        LockType obj2 = new LockType();

        // 对象锁:obj1和obj2不互斥,可以同时执行
        new Thread(() -> obj1.instanceMethod()).start();
        new Thread(() -> obj2.instanceMethod()).start();

        // 类锁:所有实例共享,互斥
        new Thread(() -> LockType.staticMethod()).start();
        new Thread(() -> LockType.staticMethod()).start();
    }
}

3. volatile关键字 (volatile Keyword)

3.1 volatile的作用

volatile关键字保证变量的**可见性**和**有序性**,但不保证**原子性**。

作用: 1. 可见性 - 一个线程的修改对其他线程立即可见 2. 有序性 - 禁止指令重排序 3. 不保证原子性 - 不能替代synchronized

3.2 可见性问题示例

/**
 * 可见性问题示例
 * Visibility Problem Example
 */
public class VisibilityProblem {
    // 不使用volatile:可能出现可见性问题
    private boolean flag = false;

    public void setFlag() {
        flag = true;
    }

    public void checkFlag() {
        while (!flag) {
            // 可能永远循环,因为看不到flag的变化
        }
        System.out.println("Flag is true");
    }

    public static void main(String[] args) {
        VisibilityProblem demo = new VisibilityProblem();

        // 线程1:设置flag
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            demo.setFlag();
        }).start();

        // 线程2:检查flag(可能永远看不到变化)
        new Thread(() -> {
            demo.checkFlag();
        }).start();
    }
}

使用volatile解决:

public class VisibilitySolution {
    // 使用volatile:保证可见性
    private volatile boolean flag = false;

    public void setFlag() {
        flag = true; // 修改立即刷新到主内存
    }

    public void checkFlag() {
        while (!flag) {
            // 每次循环都会从主内存读取flag的最新值
        }
        System.out.println("Flag is true");
    }
}

3.3 volatile不保证原子性

/**
 * volatile不保证原子性示例
 * volatile Does Not Guarantee Atomicity
 */
public class VolatileAtomicity {
    private volatile int count = 0;

    public void increment() {
        count++; // 不是原子操作!
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileAtomicity demo = new VolatileAtomicity();

        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    demo.increment();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        // 结果可能小于10000,因为volatile不保证原子性
        System.out.println("最终计数: " + demo.count);
    }
}

volatile适用场景: - 单写多读场景 - 状态标志位 - 不依赖当前值的操作

3.4 volatile禁止指令重排序

/**
 * volatile禁止指令重排序示例
 * volatile Prevents Instruction Reordering
 */
public class ReorderingExample {
    private int a = 0;
    private int b = 0;
    private volatile boolean flag = false;

    // 线程1
    public void writer() {
        a = 1;      // 1
        b = 2;      // 2
        flag = true; // 3 - volatile写,之前的操作不会被重排序到之后
    }

    // 线程2
    public void reader() {
        if (flag) { // volatile读,之后的操作不会被重排序到之前
            int sum = a + b; // 保证看到a=1, b=2
        }
    }
}

4. Java内存模型(JMM)(Java Memory Model)

4.1 JMM概述

**Java内存模型(JMM)**定义了Java虚拟机如何与计算机内存交互,规定了多线程环境下变量的读写规则。

4.2 内存区域划分

┌─────────────────────────────────────┐
│         主内存(Main Memory)          │
│  共享变量(所有线程可见)              │
└─────────────────────────────────────┘
           ↑                    ↓
           │                    │
    ┌──────┴──────┐      ┌──────┴──────┐
    │  线程1      │      │  线程2      │
    │ ┌─────────┐ │      │ ┌─────────┐ │
    │ │工作内存  │ │      │ │工作内存  │ │
    │ │本地变量  │ │      │ │本地变量  │ │
    │ │副本      │ │      │ │副本      │ │
    │ └─────────┘ │      │ └─────────┘ │
    └─────────────┘      └─────────────┘

内存区域: 1. 主内存(Main Memory) - 所有线程共享,存储共享变量 2. 工作内存(Working Memory) - 线程私有,存储变量的副本

4.3 内存交互操作

JMM定义了8种内存操作:

  1. lock(锁定) - 作用于主内存,标识变量为线程独占
  2. unlock(解锁) - 作用于主内存,释放锁定状态
  3. read(读取) - 作用于主内存,将变量值传输到工作内存
  4. load(载入) - 作用于工作内存,将read的值放入工作内存变量副本
  5. use(使用) - 作用于工作内存,将变量值传递给执行引擎
  6. assign(赋值) - 作用于工作内存,将执行引擎的值赋给变量
  7. store(存储) - 作用于工作内存,将变量值传输到主内存
  8. write(写入) - 作用于主内存,将store的值写入主内存变量

操作顺序:

read → load → use → assign → store → write

4.4 可见性问题的根源

/**
 * 可见性问题的根源
 * Root Cause of Visibility Problem
 */
public class JMMExample {
    private int count = 0; // 主内存中的变量

    public void increment() {
        // 线程1的工作内存
        int temp = count;  // read + load:从主内存读取到工作内存
        temp = temp + 1;   // use + assign:在工作内存中计算
        count = temp;      // store + write:写回主内存(可能延迟)
    }

    public int getCount() {
        // 线程2的工作内存
        return count;      // read + load:从主内存读取(可能读到旧值)
    }
}

问题: 工作内存的更新可能不会立即刷新到主内存,导致其他线程看不到最新值。


5. happens-before规则 (happens-before Rules)

5.1 什么是happens-before?

**happens-before**是JMM定义的内存可见性规则,如果操作A happens-before操作B,那么A的结果对B可见。

5.2 happens-before规则

1. 程序顺序规则(Program Order Rule)

int a = 1;  // 1
int b = 2;  // 2
// 1 happens-before 2

规则: 同一线程中,前面的操作happens-before后面的操作。

2. 监视器锁规则(Monitor Lock Rule)

synchronized (lock) {
    // 操作A
}
// 操作A happens-before 释放锁

synchronized (lock) {
    // 操作B
}
// 获取锁 happens-before 操作B

规则: 解锁操作happens-before后续的加锁操作。

3. volatile变量规则(Volatile Variable Rule)

volatile int flag = 0;

// 线程1
flag = 1; // volatile写

// 线程2
if (flag == 1) { // volatile读
    // 能看到flag=1
}

规则: volatile写happens-before后续的volatile读。

4. 线程启动规则(Thread Start Rule)

Thread thread = new Thread(() -> {
    // 操作B
});
// 操作A
thread.start(); // 操作A happens-before 操作B

规则: start()调用happens-before线程中的任何操作。

5. 线程终止规则(Thread Termination Rule)

Thread thread = new Thread(() -> {
    // 操作A
});
thread.start();
thread.join(); // 操作A happens-before join()返回
// 操作B

规则: 线程中的所有操作happens-before其他线程的join()返回。

6. 中断规则(Interruption Rule)

thread.interrupt(); // 操作A
// 操作A happens-before 线程检测到中断

规则: interrupt()调用happens-before被中断线程检测到中断。

7. 传递性规则(Transitivity Rule)

// 如果 A happens-before B
// 且 B happens-before C
// 那么 A happens-before C

6. synchronized和volatile的区别 (synchronized vs volatile)

6.1 对比表

特性 synchronized volatile
原子性 ✅ 保证 ❌ 不保证
可见性 ✅ 保证 ✅ 保证
有序性 ✅ 保证 ✅ 保证
性能 较低(重量级锁) 较高(轻量级)
使用场景 多写场景、复杂同步 单写多读、状态标志
锁机制 互斥锁 无锁

6.2 使用建议

使用synchronized: - 需要保证原子性 - 多线程写操作 - 复杂的同步逻辑

使用volatile: - 单写多读场景 - 状态标志位 - 不依赖当前值的操作

示例:单例模式(双重检查锁定)

/**
 * 双重检查锁定单例模式
 * Double-Checked Locking Singleton
 */
public class Singleton {
    private static volatile Singleton instance; // 必须使用volatile

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 需要volatile保证可见性
                }
            }
        }
        return instance;
    }
}

为什么需要volatile?

new Singleton()不是原子操作,包含: 1. 分配内存空间 2. 初始化对象 3. 将引用赋值给instance

如果没有volatile,步骤2和3可能重排序,导致其他线程看到未初始化的对象。


7. 最佳实践 (Best Practices)

7.1 减少锁的粒度

// ❌ 不推荐:锁范围太大
public synchronized void process() {
    // 不需要同步的代码
    doSomething();
    // 需要同步的代码
    synchronized (this) {
        count++;
    }
    // 不需要同步的代码
    doSomethingElse();
}

// ✅ 推荐:只锁必要的代码
public void process() {
    doSomething();
    synchronized (this) {
        count++;
    }
    doSomethingElse();
}

7.2 避免死锁

// ❌ 不推荐:可能死锁
public void method1() {
    synchronized (lockA) {
        synchronized (lockB) {
            // ...
        }
    }
}

public void method2() {
    synchronized (lockB) {
        synchronized (lockA) { // 不同的锁顺序,可能死锁
            // ...
        }
    }
}

// ✅ 推荐:统一锁顺序
public void method1() {
    synchronized (lockA) {
        synchronized (lockB) {
            // ...
        }
    }
}

public void method2() {
    synchronized (lockA) { // 相同的锁顺序
        synchronized (lockB) {
            // ...
        }
    }
}

7.3 使用final字段

// ✅ 推荐:final字段天然线程安全
public class SafeObject {
    private final int value; // final字段,线程安全

    public SafeObject(int value) {
        this.value = value;
    }
}

7.4 避免在循环中加锁

// ❌ 不推荐:在循环中加锁
public void process(List<String> items) {
    for (String item : items) {
        synchronized (this) {
            processItem(item);
        }
    }
}

// ✅ 推荐:在循环外加锁
public void process(List<String> items) {
    synchronized (this) {
        for (String item : items) {
            processItem(item);
        }
    }
}

8. 面试高频问题 (Interview Questions)

Q1: synchronized和volatile的区别?

答案:6. synchronized和volatile的区别

Q2: volatile能保证原子性吗?

答案: 不能。volatile只保证可见性和有序性,不保证原子性。例如count++不是原子操作。

Q3: 什么是Java内存模型(JMM)?

答案: JMM定义了Java虚拟机如何与内存交互,规定了多线程环境下变量的读写规则,包括主内存和工作内存的概念。

Q4: happens-before规则有哪些?

答案: 程序顺序规则、监视器锁规则、volatile变量规则、线程启动规则、线程终止规则、中断规则、传递性规则。

Q5: 双重检查锁定为什么需要volatile?

答案: 防止指令重排序,确保其他线程看到完全初始化的对象。


📖 扩展阅读


返回: 07-Java并发编程
上一章: 07-01 - 线程基础
下一章: 07-03 - 锁机制 →