
本文通过对比静态共享锁与实例独占锁在多线程环境下的行为差异,深入解释为何使用 static Object lock 可实现串行化输出,而每个线程持有独立的 lock2 实例则导致竞态与输出交错。
本文通过对比静态共享锁与实例独占锁在多线程环境下的行为差异,深入解释为何使用 `static object lock` 可实现串行化输出,而每个线程持有独立的 `lock2` 实例则导致竞态与输出交错。
在 Java 多线程编程中,synchronized 的同步效果完全取决于被锁定对象的“身份一致性”——即多个线程是否竞争同一把“锁”。关键不在于锁变量名是否相同,而在于它们是否引用同一个对象实例。
? 核心原理:锁对象的“唯一性”决定同步粒度
✅ 静态锁(private static Object lock)
所有 Worker 实例(即所有线程)都通过 MyClass.lock 访问同一个 Object 实例。当任一线程进入 synchronized(lock) 块时,它会独占该对象监视器(monitor),其他线程必须等待其释放锁后才能进入——从而实现对 System.out.format(...) 这一临界区的互斥访问,输出严格按时间顺序串行化。❌ 实例锁(private Object lock2 = new Object())
每个 Worker 对象在构造时创建各自独立的 lock2 实例。5 个线程对应 5 个不同的 Object 锁对象。synchronized(lock2) 实际上是 5 把互不相关的“钥匙”,各线程锁住自己的锁、执行自己的打印,彼此无感知——根本不存在竞争,也就没有同步效果,输出自然重叠、交错。
? 验证代码(修正版对比示例)
class MyClass {
private static final Object SHARED_LOCK = new Object(); // ✅ 全局唯一锁
public static void main(String[] args) {
// 启动 5 个 Worker 线程
for (int i = 1; i <= 5; i++) {
new Thread(new Worker(SHARED_LOCK), "Thread-" + i).start();
}
}
static class Worker implements Runnable {
private final Object lock; // ⚠️ 锁对象由外部传入,确保复用
private int runCount = 1;
Worker(Object lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) { // 减少次数便于观察
synchronized (lock) {
System.out.format("? [%s] runCount = %d%n",
Thread.currentThread().getName(), runCount++);
// 可选:添加微小延迟增强竞态可见性
try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
}
}
}? 若将 Worker 改为使用 new Object() 作为锁(即原题中的 lock2),即使仅运行一次,你也会看到类似 Thread-3: runCount = 1、Thread-1: runCount = 1、Thread-2: runCount = 1 的重复输出——因为每个线程的 runCount 是独立字段,且无跨线程保护。
⚠️ 注意事项与最佳实践
- 避免“假同步”陷阱:声明为 private Object lock = new Object() 的实例变量,永远无法用于跨线程同步,除非显式共享该对象引用(如通过构造函数注入)。
- 优先使用 private static final 锁对象:语义清晰、生命周期匹配类级别同步需求;final 可防止意外重赋值。
- 慎用 this 或 getClass() 作为锁:易引发锁泄露或死锁(尤其在继承场景下);推荐使用专用的、私有的锁对象。
- 日志/打印本身不是原子操作:System.out.format(...) 内部涉及 I/O 和字符串拼接,若未加锁,在高并发下可能产生乱序或截断输出——这正是本例中需要同步的关键原因。
✅ 总结
同步的本质是“争抢同一把锁”。静态锁因对象唯一而形成有效互斥;实例锁因对象分散而形同虚设。理解 synchronized(obj) 中 obj 的运行时身份(identity),而非声明位置,是掌握 Java 并发控制的第一道门槛。实践中,请始终问自己:“此刻有多少个线程正在尝试锁住同一个对象?” ——答案决定了你的代码是否真正线程安全。










