在前两篇文章中,我们学习了多线程的三种创建方式以及线程的生命周期与状态转换,掌握了线程从创建到终止的全过程。然而,当多个线程并发访问共享资源时,如果没有适当的协调机制,就可能出现数据不一致、竞态条件(Race Condition) 等严重问题。

本文将深入探讨 Java 中最基础也是最重要的线程同步机制——synchronized 关键字,帮助你理解它是如何保证原子性、可见性与有序性,从而实现线程安全的。


一、为什么需要线程同步?

考虑以下场景:两个线程同时对一个共享变量 count 执行自增操作:

public class Counter {
    private int count = 0;

    public void increment() {
        count++; // 非原子操作:读取 -> 修改 -> 写入
    }

    public int getCount() {
        return count;
    }
}
Counter counter = new Counter();
Runnable task = () -> {
    for (int i = 0; i < 10000; i++) {
        counter.increment();
    }
};

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);

t1.start();
t2.start();
t1.join();
t2.join();

System.out.println("最终结果: " + counter.getCount()); // 很可能小于 20000!

问题分析count++ 并非原子操作,它包含三步:

  1. 读取 count 的值
  2. 将值加 1
  3. 写回内存

多个线程可能同时读取到相同的旧值,导致“丢失更新”。

这就是典型的竞态条件。解决方法就是使用同步机制,确保同一时刻只有一个线程能执行该操作。


二、synchronized 的核心作用

synchronized 是 Java 内建的 互斥锁(Mutex) 机制,它能确保:

  • 原子性(Atomicity):多个操作作为一个整体执行,不会被其他线程打断。
  • 可见性(Visibility):一个线程修改共享变量后,其他线程能立即看到最新值。
  • 有序性(Ordering):防止指令重排序带来的问题(配合 happens-before 规则)。

💡 synchronized 的底层依赖于 监视器锁(Monitor Lock),每个 Java 对象都关联一个监视器。


三、synchronized 的三种使用方式

1. 修饰实例方法

锁住的是当前实例对象(this)

public class SynchronizedCounter {
    private int count = 0;

    // 锁住 this 对象
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

✅ 适用场景:多个线程操作同一个对象实例的方法。


2. 修饰静态方法

锁住的是该类的 Class 对象(即 SynchronizedCounter.class)。

public class SynchronizedCounter {
    private static int totalCount = 0;

    // 锁住 SynchronizedCounter.class
    public static synchronized void incrementTotal() {
        totalCount++;
    }
}

✅ 适用场景:保护类级别的共享资源,多个实例共享同一份静态数据。


3. 同步代码块(推荐)

手动指定锁对象,粒度更细,性能更高。

public class FineGrainedCounter {
    private int count = 0;
    private final Object lock = new Object(); // 专用锁对象

    public void increment() {
        synchronized (lock) { // 显式加锁
            count++;
        }
    }
}

✅ 推荐使用专用对象作为锁,避免暴露 thisClass 引用,提升安全性与灵活性。

⚠️ 不推荐:使用字符串常量或 Integer 等缓存对象作为锁,可能导致意外的锁竞争。


四、synchronized 的底层原理简析

synchronized 的实现依赖于 JVM 的 监视器(Monitor) 机制,其核心是 monitor entermonitor exit 字节码指令。

当线程进入 synchronized 块时:

  1. 尝试获取对象的监视器锁。
  2. 若锁空闲,则获取成功,进入临界区。
  3. 若已被其他线程持有,则进入 BLOCKED 状态,等待锁释放。

🔍 从 JDK 1.6 开始,synchronized 经历了优化,引入了:

  • 偏向锁(Biased Locking)
  • 轻量级锁(Lightweight Locking)
  • 重量级锁(Heavyweight Locking)

实现了锁的升级机制,在无竞争时开销极小,有竞争时自动升级,兼顾性能与安全性。


五、代码示例:对比同步与非同步

public class SyncDemo {
    private int unsafeCount = 0;
    private int safeCount = 0;
    private final Object lock = new Object();

    // 非同步方法:线程不安全
    public void unsafeIncrement() {
        unsafeCount++;
    }

    // 同步方法:线程安全
    public synchronized void safeIncrement() {
        safeCount++;
    }

    public static void main(String[] args) throws InterruptedException {
        SyncDemo demo = new SyncDemo();

        Runnable unsafeTask = () -> {
            for (int i = 0; i < 10000; i++) {
                demo.unsafeIncrement();
            }
        };

        Runnable safeTask = () -> {
            for (int i = 0; i < 10000; i++) {
                demo.safeIncrement();
            }
        };

        Thread t1 = new Thread(unsafeTask);
        Thread t2 = new Thread(unsafeTask);
        Thread t3 = new Thread(safeTask);
        Thread t4 = new Thread(safeTask);

        t1.start(); t2.start();
        t3.start(); t4.start();

        t1.join(); t2.join();
        t3.join(); t4.join();

        System.out.println("非同步结果: " + demo.unsafeCount); // 可能 < 20000
        System.out.println("同步结果: " + demo.safeCount);     // 一定是 20000
    }
}

输出示例:

非同步结果: 18364
同步结果: 20000

✅ 只有使用 synchronized 才能保证最终结果的正确性。


六、常见误区与最佳实践

❌ 误区1:synchronized 锁住的是代码

✘ 错误理解:synchronized 保护的是某段代码。

✅ 正确理解:它保护的是对象的监视器。多个线程要竞争的是同一个锁对象,才能互斥。

synchronized (new Object()) { // 每次都新建对象,锁无效!
    count++;
}

❌ 上述代码每次创建新对象,各线程持有不同的锁,无法实现同步。


❌ 误区2:synchronized 方法比代码块更高效

✘ 事实:synchronized 方法锁的范围更大(整个方法),可能导致不必要的阻塞。

✅ 建议:优先使用同步代码块,只锁住关键代码段,减少锁的持有时间。


✅ 最佳实践总结

实践说明
使用 private final Object 作为锁避免外部干扰,防止锁被恶意释放
避免使用 StringInteger 等缓存对象作为锁可能引发意外的锁竞争
减少同步代码块的粒度提高并发性能
避免在同步块中调用外部方法防止死锁或延长锁持有时间

七、synchronized 的局限性

虽然 synchronized 简单易用,但也存在一些限制:

  • ❌ 无法尝试获取锁(无 tryLock
  • ❌ 无法中断正在等待锁的线程
  • ❌ 不支持公平锁(默认非公平)
  • ❌ 锁的粒度较粗,难以实现复杂同步逻辑

八、总结

特性synchronized
使用方式方法、静态方法、代码块
锁对象this / Class / 指定对象
原子性
可见性
可中断等待
超时获取
公平性❌(默认非公平)
底层机制Monitor(JVM 内建)

synchronized 是 Java 多线程编程的基石。理解其工作原理和使用场景,是构建线程安全程序的第一步。它虽然简单,但威力强大,是每个 Java 开发者必须掌握的核心技能。