引言:并发编程的三大挑战
在多线程编程中,我们常常面临三个核心问题:
-
可见性
:一个线程对共享变量的修改,是否能被其他线程立即看到?
-
原子性
:一个操作是否不可被中断?
-
有序性
:程序执行的顺序是否符合预期?
Java 提供了三大关键字来帮助我们应对这些挑战:volatile、synchronized 和 final。它们不仅是面试高频考点,更是构建高质量并发程序的基石。
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+):
-
偏向锁
:无竞争时,线程独占对象,几乎无开销
-
轻量级锁
:有竞争但无阻塞,使用 CAS 尝试获取锁
-
重量级锁
:多线程竞争激烈时,线程阻塞挂起
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字段就不会出现“半初始化”状态
使用场景
-
构建不可变对象(如
String、Integer、LocalDate) -
安全发布单例对象
-
作为线程间共享数据的只读快照
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不能替代
synchronized或volatile,它只适用于构造时的只读语义final List list = new ArrayList<>();list.add("mutable"); // 合法,引用不可变但对象可变