并发编程(四)

前言

并发编程是Java进阶的必经之路,也是面试中的重中之重。许多开发者对`volatile`、`synchronized`等关键字仅停留在"能保证可见性"的浅显理解,对于其底层的内存模型和指令重排序规则却知之甚少。

本文将基于Java内存模型(JMM),从底层硬件架构讲起,深入探讨指令重排序、内存屏障、`happens-before`原则,并结合单例模式(DCL)的终极写法,带你领略并发编程的"至高境界"。

1. Java内存模型(JMM)与三大特性

1.1 为什么需要内存模型?

由于CPU的计算速度远快于内存的读写速度,现代计算机都引入了高速缓存(Cache) 和流水线技术。这导致了两个核心问题:

  1. 可见性问题:一个线程对共享变量的修改,另一个线程无法立即看到。

  2. 有序性问题:为了提升性能,编译器和处理器会对指令进行重排序。

Java内存模型(JMM)是一套规范,它屏蔽了不同硬件和操作系统的差异,确保了Java程序在各种平台下都能有一致的并发表现。JMM的核心目标是保证程序的原子性、可见性和有序性。

1.2 指令重排序与可见性危机

指令重排序在单线程下能保证执行结果不变,但在多线程下可能导致灾难性后果。

经典示例:

两个线程并发执行以下代码,可能出现 `x = y = 0` 的结果。

java 复制代码
```java
// 共享变量
int a = 0, b = 0;
int x = 0, y = 0;

// 线程A执行
a = 1;
x = b;

// 线程B执行
b = 2;
y = a;
```

| 可能的执行顺序 | 结果 (x, y) |
| :--- | :--- |
| A1->A2->B1->B2 | (0, 1) |
| B1->B2->A1->A2 | (2, 0) |
| A1->B1->A2->B2 | (2, 1) |
| **A1->B1->B2->A2(重排序)** | **(0, 0)** |

分析:

线程A的代码`a=1`和`x=b`在处理器执行时可能被重排序,先执行`x=b`(此时b=0)。线程B同理,先执行`y=a`(此时a=0)。最终导致`x`和`y`都为0,完全违背了程序员的直觉。

结论:重排序提升了性能,但破坏了多线程程序的正确性。我们需要一种机制来禁止特定场景下的指令重排序。

2. 破解之道:内存屏障与Happens-Before

2.1 内存屏障(Memory Barrier)

内存屏障是一种CPU指令,它就像一个栅栏,告诉编译器和CPU:屏障前后的指令禁止重排序。

JMM将内存屏障分为四类:

读读屏障 (LoadLoad):禁止Load1和Load2重排序。

读写屏障 (LoadStore):禁止Load1和Store2重排序。

写写屏障 (StoreStore):禁止Store1和Store2重排序。

写读屏障 (StoreLoad):禁止Store1和Load2重排序。这是一个"全能型"屏障,开销最大,但能实现其他所有屏障的效果。

2.2 Happens-Before原则

`happens-before`是JMM的核心概念。如果两个操作满足`happens-before`关系,那么第一个操作的执行结果对第二个操作一定是可见的,且第一个操作的执行顺序在第二个操作之前。

关键规则:

  1. 程序次序规则:一个线程内,书写在前的代码`happens-before`于书写在后的代码。

  2. Volatile变量规则:对一个`volatile`变量的写操作,`happens-before`于后续对这个`volatile`变量的读操作。

  3. 锁规则:对一个锁的解锁(`unlock`)`happens-before`于后续对这个锁的加锁(`lock`)。

  4. 传递性:如果A `happens-before` B,且B `happens-before` C,那么A `happens-before` C。

> 面试点:`happens-before`并不是指令执行的先后顺序,而是结果的可见性保证。

3. volatile关键字深度解析

volatile是轻量级的同步机制,它保证了两件事:

  1. 可见性 :对一个volatile变量的写,会立即刷新到主内存;读操作会从主内存读取,保证读到最新值。

  2. 有序性:通过禁止指令重排序来实现。

3.1 禁止重排序规则

volatile通过插入内存屏障来限制重排序,其核心规则是:

  • 在每个volatile写操作前插入StoreStore屏障
  • 在每个volatile写操作后插入StoreLoad屏障
  • 在每个volatile读操作后插入LoadLoadLoadStore屏障

示例:修复可见性问题

java 复制代码
class VolatileExample {
  int a = 0;
  volatile boolean flag = false;

  public void writer() {
    a = 1;     // 普通写
    flag = true;  // volatile写
    // 由于volatile写,a=1 不会被重排序到 flag=true 之后
  }

  public void reader() {
    if (flag) {   // volatile读
      int i = a; // 此时 a 一定为 1
      System.out.println(i);
    }
  }
}

3.2 volatile与synchronized的区别

特性 volatile synchronized
原子性 不保证(如count++非原子) 保证,被修饰的代码块是原子的
可见性 保证 保证
有序性 保证(禁止重排序) 保证(同一时刻单线程执行)
阻塞 不阻塞,轻量级 阻塞,重量级
适用场景 单个变量的读写,状态标记 复合操作(读-改-写),代码块互斥

一句话总结volatile解决的是可见性和有序性问题,synchronized解决的是原子性问题。

4. 单例模式(DCL)的终极写法:并发编程的"至高境界"

单例模式与双重检查锁(DCL)实现

4.1 为什么需要volatile

以下是一个错误的DCL实现:

java 复制代码
public class Singleton {  
  private static Singleton instance;  
  private Singleton() {}  
  public static Singleton getInstance() {  
    if (instance == null) {     // 第一次检查  
      synchronized (Singleton.class) {  
        if (instance == null) { // 第二次检查  
          instance = new Singleton(); // 问题出在这里!  
        }  
      }  
    }  
    return instance;  
  }  
}  

问题所在

instance = new Singleton(); 这一行代码在JVM中并非原子操作,它大致分为三步:

  1. 分配内存 :为Singleton对象分配一块内存空间。
  2. 初始化对象:调用构造函数,对对象进行初始化。
  3. 建立关联 :将instance引用指向这块内存地址。

步骤2和3在指令层面可能发生重排序!执行顺序可能变为 1 -> 3 -> 2。

引发错误

  • 线程A执行到instance = new Singleton();,发生了重排序,先执行了步骤3(此时内存空间还未初始化,instance不为null,但对象内部都是默认值)。
  • 此时线程B调用getInstance(),在第一次检查时发现instance != null,直接返回。
  • 线程B拿着一个尚未初始化完成的对象去操作,程序必然崩溃。

4.2 正确的DCL终极写法

java 复制代码
public class Singleton {  
  // 关键点1:使用 volatile 禁止指令重排序  
  private static volatile Singleton instance;  

  // 关键点2:构造方法私有,防止外部 new  
  private Singleton() {}  

  public static Singleton getInstance() {  
    // 第一次检查:避免不必要的锁竞争  
    if (instance == null) {  
      // 关键点3:同步代码块,而不是同步方法,粒度更细,性能更好  
      synchronized (Singleton.class) {  
        // 第二次检查:防止在等待锁的过程中,其他线程已经创建了实例  
        if (instance == null) {  
          // volatile 保证了这一步的初始化操作不会被重排序  
          instance = new Singleton();  
        }  
      }  
    }  
    return instance;  
  }  
}  
逐行解释(面试必考)
  1. private static volatile Singleton instance;:

    • private:外部无法直接访问。
    • static:类级别共享。
    • volatile核心 。禁止new Singleton()时的指令重排序,保证对象初始化完成后再将引用赋值给instance
  2. private Singleton() {}:

    • 构造方法私有,确保无法通过new生成新实例,只能通过getInstance()获取。
  3. 第一次if (instance == null):

    • 性能优化。如果实例已存在,直接返回,无需进入同步块。
  4. synchronized (Singleton.class):

    • 加锁粒度是代码块,而非整个方法。对象创建完成后,后续的并发读操作将不再需要排队,提升性能。
  5. 第二次if (instance == null):

    • 关键防护。考虑线程A和B同时通过了第一次检查,A进入同步块并创建了对象,释放锁后,B获得锁,必须再次检查,否则B会再创建一个对象。

5. 锁的底层:AQS与CAS

5.1 AQS(AbstractQueuedSynchronizer)

`ReentrantLock`、`CountDownLatch`等同步工具的底层都是AQS。它提供了一个FIFO队列(CLH队列)来管理阻塞的线程,并内置了一个`volatile int state`变量来表示同步状态(例如锁的重入次数)。

5.2 CAS(Compare And Swap)

CAS是一条CPU原子指令,它包含三个操作数:内存地址V,期望值A,新值B。如果V上的值等于A,则将其更新为B,否则什么都不做。整个过程是原子的。

CAS + 自旋 = 乐观锁的基础:

java 复制代码
```java
// 模拟一个非阻塞的原子自增操作
public void increment() {
    int oldValue;
    int newValue;
    do {
        oldValue = get(); // 获取当前值
        newValue = oldValue + 1;
    } while (!compareAndSwap(oldValue, newValue)); // 自旋,直到设置成功
}
```

5.3 公平锁 vs 非公平锁

公平锁:线程严格按照申请锁的顺序获取锁。实现复杂,会频繁进行线程上下文切换,性能较低。

非公平锁:新来的线程有机会直接抢占锁。减少了上下文切换,整体吞吐量更高,是主流实现(如`ReentrantLock`默认就是非公平锁)。缺点是可能造成"线程饥饿"。

6. 总结

  1. JMM是基石:理解了JMM、重排序、内存屏障和`happens-before`,才能真正看透并发问题。

  2. volatile轻量级:适合修饰"状态标记"和一个不依赖当前值的"读写操作"。

  3. synchronized`重量级:保证了原子性,适合复合操作,但性能相对较低。现代JVM对其优化后(锁升级),性能已大幅提升。

  4. DCL单例是试金石:考察了`volatile`、锁、指令重排序、原子性等多个并发核心概念,手写并解释其原理是高级工程师的必备技能。

  5. AQS与CAS是骨架:J.U.C包下几乎所有高级并发工具都基于AQS和CAS构建,理解它们就掌握了并发编程的半壁江山。

希望这篇博客能帮助你彻底拿下Java并发编程的核心知识点!

相关推荐
葱卤山猪1 小时前
C++17 联合体
开发语言·c++
折哥的程序人生 · 物流技术专研1 小时前
Java 23 种设计模式:从踩坑到精通 | 抽象工厂 —— 支付/收款如何成套创建?跨平台 UI 如何一键换肤?
java·开发语言·后端·设计模式
方也_arkling1 小时前
【Java-Day11】抽象类和抽象方法
java·开发语言
Ulyanov2 小时前
深入QML-Python通信 构建响应式交互界面的桥梁设计:QML+PySide6现代开发入门(五)
开发语言·python·算法·交互·qml·系统仿真
就叫_这个吧2 小时前
JavaScript中常用事件示例展示附源码
开发语言·javascript·html
浩瀚之水_csdn2 小时前
Python 推导式详解:从入门到精通
python
不会C语言的男孩2 小时前
C++ Primer Plus 第9章:内存模型和名称空间
开发语言·c++
zz34572981132 小时前
函数:python与c语言
c语言·开发语言·python
li星野2 小时前
LLMLingua:用小型模型“剪枝”大语言模型提示词,让长文本不再昂贵
人工智能·python·学习·语言模型·剪枝