"synchronized 是什么意思?"——一个困扰着无数初学者的经典问题
\n\n如果你曾经用 Java 写过多线程应用程序,很可能遇到过这种情况:两个线程同时修改同一个变量,导致 Bug 在运行一百次后才重现一次。听起来熟悉吗?这正是 synchronized 关键字的作用。但它到底做了什么?"底层"是如何工作的?最重要的是——什么时候该使用它,什么时候最好避免使用?
在本文中,我们不会只是复述文档。我们将解读一个热门的 Stack Overflow 问题(一个吸引了数千次浏览的问题),将其改编成教学形式,并展示不同方法的对比:方法级同步、块级同步,以及来自 java.util.concurrent 的替代方案。让我们开始吧!
问题:竞态条件
\n\n想象一下:你有一个计数器,由两个线程递增。代码很简单,但结果却出乎意料。
\n\npublic class Counter {\n private int count = 0;\n\n public void increment() {\n count++;\n }\n\n public int getCount() {\n return count;\n }\n}\n\n我们运行两个线程,每个执行 1000 次递增。你期望结果是 2000。但实际上可能是 1987、1993,甚至 1765。为什么?因为 count++ 操作不是原子性的。它由三步组成:读取 → 递增 → 写入。线程"重叠"在一起,导致一个更改丢失了。
这就是竞态条件。而 synchronized 是对抗它的主要工具之一。
synchronized 做了什么?让我们看看底层
\n\nJava 中的 synchronized 关键字保证了互斥访问(互斥锁)以及线程之间变更的可见性。当一个线程进入一个 synchronized 块时,它会获取对象(或类)的监视器。其他试图进入同一监视器上任何 synchronized 块的线程将被阻塞,直到该监视器被释放。
重要提示:synchronized 只与一个监视器配合工作。如果你有两个不同的对象——同步将不起作用。
两种同步级别
\n- \n
- 同步方法——锁定整个方法,使用对象自身的监视器(
this)。 \n - 同步块——只锁定部分代码,你可以指定任何对象作为监视器。 \n
让我们在实践中比较它们。
\n\n方法对比:方法级 vs 块级
\n\n1. 同步方法
\n\n最简单的方法——在方法签名中添加 synchronized。
public class SafeCounter {\n private int count = 0;\n\n public synchronized void increment() {\n count++;\n }\n\n public synchronized int getCount() {\n return count;\n }\n}\n\n优点:
\n- \n
- 代码量最小,易于阅读。 \n
- 保证整个方法是一个原子操作。 \n
缺点:
\n- \n
- 锁的"重量"很大——即使方法只有一行代码,线程在整个执行期间都会被阻塞。 \n
- 如果方法很长或调用频繁,会降低性能。 \n
- 不够灵活——你无法只同步部分逻辑。 \n
2. 同步块
\n\n更细粒度的控制。你可以决定将哪个对象用作监视器。
\n\npublic class SafeCounterWithBlock {\n private int count = 0;\n private final Object lock = new Object();\n\n public void increment() {\n synchronized (lock) {\n count++;\n }\n }\n\n public int getCount() {\n synchronized (lock) {\n return count;\n }\n }\n}