Java中的synchronized关键字
字数 1924 2025-11-02 13:21:23
Java中的synchronized关键字
synchronized关键字是Java中用于实现线程同步的核心机制,它可以确保多个线程在访问共享资源时的互斥性,从而避免数据不一致的问题。
1. 为什么需要synchronized?
在多线程环境中,当多个线程同时读写同一个共享资源(例如一个对象的成员变量)时,可能会发生“竞态条件”。这是因为一个简单的操作(如 count++)在底层实际上分为三个步骤:读取当前值、将值加一、写回新值。如果两个线程交错执行这三个步骤,就可能导致最终结果错误。
2. synchronized的三种应用方式
synchronized的核心理念是“锁”。一个线程在进入被synchronized保护的代码块前必须先获得锁,在退出代码块(包括正常退出和异常退出)时会自动释放锁。其他试图获取同一把锁的线程将被阻塞等待。
-
方式一:同步实例方法
- 作用范围:整个实例方法。
- 锁对象:方法所属对象的实例(即
this)。 - 代码示例:
public class Counter { private int count = 0; // 同步实例方法 public synchronized void increment() { count++; // 此方法同一时刻只能被一个线程执行 } } - 解释:假设有两个线程Thread-A和Thread-B,它们操作的是同一个Counter对象
counter。当Thread-A进入increment方法时,它会获取counter对象实例的锁。此时如果Thread-B也试图调用counter.increment(),它会被阻塞,直到Thread-A执行完方法并释放锁。
-
方式二:同步静态方法
- 作用范围:整个静态方法。
- 锁对象:方法所属类的Class对象(如
Counter.class)。 - 代码示例:
public class Counter { private static int staticCount = 0; // 同步静态方法 public static synchronized void staticIncrement() { staticCount++; // 此方法同一时刻只能被一个线程执行 } } - 解释:静态方法是属于类的,而不是某个实例。因此,它的锁是类的Class对象。无论创建了多少个Counter实例,这个锁只有一把。这意味着,一个线程在执行
staticIncrement时,所有其他线程都不能执行任何Counter类的同步静态方法。
-
方式三:同步代码块
- 作用范围:代码块
{}包围的部分,更加灵活。 - 锁对象:可以指定任意对象(
synchronized (lockObject)),通常使用专门的对象来增加清晰度和控制粒度。 - 代码示例:
public class FineGrainedCounter { private int count1 = 0; private int count2 = 0; // 创建两个不同的锁对象 private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void increment1() { synchronized (lock1) { // 使用lock1作为锁 count1++; } } public void increment2() { synchronized (lock2) { // 使用lock2作为锁 count2++; } } } - 解释:这种方式提供了更细粒度的锁控制。
increment1和increment2方法使用不同的锁(lock1和lock2),因此它们可以并发执行,互不干扰。这比直接锁住整个对象(synchronized (this))或整个方法的性能更好。
- 作用范围:代码块
3. 深入理解:锁的存储与特性
在JVM中,每个对象在堆内存中都有一个对象头,其中一部分叫做“Mark Word”,它就用来存储锁的信息。这解释了为什么Java中的任何对象都可以作为锁。
synchronized锁在JDK 1.6之后进行了重大优化,引入了“锁升级”机制,使得它的性能得到了极大提升。其升级路径是:
- 无锁状态:刚开始,对象没有被任何线程锁定。
- 偏向锁:如果一个线程获得了锁,锁会进入偏向模式,并记录这个线程的ID。以后该线程再次请求锁时,无需任何同步操作,直接获取,开销极小。适用于只有一个线程访问同步块的场景。
- 轻量级锁:当有第二个线程尝试获取锁时(发生锁竞争,但竞争不激烈),偏向锁会升级为轻量级锁。线程会通过“自旋”(循环尝试)的方式等待锁释放,避免了操作系统层面昂贵的线程切换。
- 重量级锁:如果自旋等待时间过长(竞争激烈),或者有多个线程同时竞争,轻量级锁会升级为重量级锁。此时,未能获取锁的线程会被挂起,进入阻塞状态,等待操作系统调度唤醒。这是早期synchronized效率低下的原因。
4. 总结与最佳实践
- 作用:synchronized保证了原子性(同步块内的操作不可分割)和可见性(一个线程修改了共享变量后,其他线程能立即看到最新值)。
- 可重入性:同一个线程可以多次获取同一把锁(例如,一个同步方法可以调用另一个同步方法),不会把自己锁死。
- 非公平锁:synchronized是非公平锁,即等待的线程不是按先来后到的顺序获取锁,这可能导致某些线程“饥饿”。优点是吞吐量通常更高。
- 最佳实践:
- 减小同步范围:只同步有线程安全问题的关键代码,而不是整个方法。
- 使用专用锁对象:对于同步代码块,使用
private final的成员变量作为锁对象,避免使用this或可被外部访问的对象,以防止死锁。 - 考虑并发工具包:对于复杂的并发场景(如读写锁、信号量等),
java.util.concurrent包提供了更强大、更灵活的工具(如ReentrantLock),可以作为synchronized的替代或补充。