在构建高性能、响应迅速的 Java 应用程序时,并发编程是无法绕开的核心技术。它允许多个线程同时执行,充分利用多核处理器资源。然而,多线程环境下共享数据的访问会带来可见性和原子性问题。Java 提供了多种机制来解决这些问题,其中 synchronized 和 volatile 是两个最基础且至关重要的关键字。理解它们的原理、差异和适用场景,是掌握 Java 并发编程的关键一步。
一、并发问题的根源:可见性与原子性
- 可见性问题:一个线程对共享变量的修改,其他线程可能无法立即看到。这通常是由于现代计算机架构(如 CPU 的多级缓存)和 Java 内存模型(JMM)的优化策略造成的。线程操作的数据可能存在于各自的工作内存中,而非直接在主内存中同步。
- 原子性问题:指一个操作(可能包含多个步骤)要么全部执行完成,要么完全不执行,中间状态不会被其他线程观察到。例如,简单的
i++操作,在底层包含读取、修改、写入三个步骤,在多线程环境下,如果没有正确同步,可能导致计数错误。
二、synchronized 关键字:重量级锁的守护者
synchronized 是 Java 提供的最基础的互斥同步机制。它可以用于修饰代码块或方法。
public class Counter {
private int count = 0;
// synchronized 修饰方法
public synchronized void increment() {
count++; // 这个操作现在是原子的
}
// synchronized 修饰代码块
public void decrement() {
synchronized (this) { // 使用当前对象实例作为锁
count--;
}
}
}
核心作用:
- 互斥性 (Mutual Exclusion):当一个线程进入
synchronized修饰的方法或代码块时,它会尝试获取与指定对象(对于实例方法或synchronized(this)是当前对象,对于静态方法是类对象)关联的监视器锁 (Monitor Lock)。获取锁成功后,其他试图获取同一把锁的线程将被阻塞,直到锁被释放。这保证了同一时刻只有一个线程能执行临界区代码,解决了原子性问题。 - 可见性保证:根据 JMM 的
happens-before原则,对一个锁的解锁操作unlock优先于后续对同一个锁的加锁操作 。这意味着,线程在释放锁时,会强制将工作内存中的共享变量刷新回主内存;而线程在获取锁时,会强制从主内存中重新读取共享变量的最新值。这解决了问题。

