Java 并发关键字:volatile、synchronized 与 final 的底层原理与应用

105 阅读4分钟

引言:并发编程的三大挑战

在多线程编程中,我们常常面临三个核心问题:

  • 可见性

    :一个线程对共享变量的修改,是否能被其他线程立即看到?

  • 原子性

    :一个操作是否不可被中断?

  • 有序性

    :程序执行的顺序是否符合预期?

Java 提供了三大关键字来帮助我们应对这些挑战:volatilesynchronizedfinal。它们不仅是面试高频考点,更是构建高质量并发程序的基石。

volatile:轻量级的可见性保障

作用

volatile 用于修饰变量,确保:

  • 对该变量的写操作对所有线程立即可见

  • 禁止指令重排序(部分)

底层原理

  • 编译器在 volatile 写操作前插入 StoreStore 屏障

  • 在读操作后插入 LoadLoad 屏障

  • 保证写入主内存、读取主内存,避免线程本地缓存失效

使用场景

  • 状态标志(如 isRunning

  • 双重检查锁(DCL)中的懒汉式单例

常见误区

  • volatile

    不能保证原子性,例如 count++ 是非原子的,即使 count 是 volatile。

  • 不适用于复合操作或依赖前后状态的逻辑。

synchronized:重量级的互斥与可见性保障

作用

synchronized 是 Java 提供的关键字,用于实现线程之间的互斥访问。它可以修饰方法或代码块,确保:

  • 同一时间只有一个线程可以执行被保护的代码(互斥性)

  • 进入和退出临界区时,线程对共享变量的修改对其他线程可见(可见性)

  • 保证代码执行的顺序符合 happens-before 规则(有序性)

底层原理

synchronized 的实现依赖于 JVM 的 Monitor(监视器锁),其本质是对对象头中的 Mark Word 进行加锁操作。

字节码指令:

  • monitorenter

    :进入同步块时执行

  • monitorexit

    :退出同步块时执行

锁的状态升级过程(JDK 1.6+):

  1. 偏向锁

    :无竞争时,线程独占对象,几乎无开销

  2. 轻量级锁

    :有竞争但无阻塞,使用 CAS 尝试获取锁

  3. 重量级锁

    :多线程竞争激烈时,线程阻塞挂起

JVM 会根据运行时的锁竞争情况自动升级或降级锁状态,以平衡性能与安全性。

使用方式

修饰实例方法:

public synchronized void increment() {    count++;}

修饰静态方法:

public static synchronized void log(String msg) {    System.out.println(msg);}

修饰代码块:

synchronized (lockObject) {    // 临界区}

JMM 视角下的行为

synchronized 保证:

  • 进入同步块之前

    ,必须先获得锁

  • 退出同步块之后

    ,会将工作内存中的变量刷新到主内存

  • 锁释放前的所有写操作

    ,对后续获得该锁的线程可见(happens-before)

常见误区

  • synchronized

    不是万能的,错误使用可能导致死锁

  • 锁对象选择不当(如使用 this 或字符串常量)可能导致锁冲突

  • 忽视锁粒度,导致性能下降

性能优化建议

  • 使用局部锁对象而非全局锁

  • 避免在高频方法中使用重量级锁

  • 使用 ReentrantLock 替代 synchronized 时可获得更高灵活性(如可中断、限时等待)

final:不可变性的线程安全基石

作用

final 是 Java 中用于定义常量和不可变性的关键字,具有以下作用:

  • 修饰变量:赋值后不可更改

  • 修饰方法:不可被子类重写

  • 修饰类:不可被继承

在并发编程中,final 的最大价值在于:构建不可变对象,从而天然具备线程安全性

与并发的关系

Java 内存模型(JMM)对 final 字段有特殊规定:

  • 构造函数完成前,其他线程不可见 final 字段
  • 构造函数完成后,final 字段对其他线程立即可见
  • 这意味着:只要对象构造完成并安全发布,final 字段就不会出现“半初始化”状态

使用场景

  • 构建不可变对象(如 StringIntegerLocalDate

  • 安全发布单例对象

  • 作为线程间共享数据的只读快照

    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; }}

JMM 视角下的行为

  • final

    字段在构造函数中初始化后,JMM 会插入写屏障,确保其对其他线程可见

  • 如果对象未被“安全发布”,即使字段是 final,也可能被其他线程看到未初始化的值

安全发布方式包括:

  • 将对象引用存入 volatile 变量

  • 使用 synchronized 保护发布过程

  • 使用线程安全容器(如 ConcurrentHashMap

  • 使用 static final 初始化(类加载阶段天然线程安全)

常见误区

  • final

    修饰引用变量,只保证引用不可变,不保证对象内容不可变

  • final

    不能替代 synchronizedvolatile,它只适用于构造时的只读语义

    final List list = new ArrayList<>();list.add("mutable"); // 合法,引用不可变但对象可变