好的,我们来深入详解 synchronized
,主要聚焦其在 Java 并发编程 中的核心作用、工作原理、使用方式和注意事项。它是 Java 中实现线程同步、保证线程安全最基础且最重要的关键字之一。
synchronized 详解 (Java 并发编程核心)
1. 核心目标:线程安全
- 问题根源: 多线程环境下,多个线程同时访问和修改共享的可变状态(变量、对象、资源)时,由于线程执行的不确定性(CPU 时间片轮换调度),可能导致数据出现不一致、错误或不可预测的结果。这就是竞态条件。
- 解决方案:
synchronized
提供了一种互斥锁机制,确保在同一时刻,最多只有一个线程可以执行被synchronized
保护的代码段或访问被保护的对象。它强制要求线程在进入同步块/方法前必须先获得锁,执行完毕后再释放锁,其他试图获得同一把锁的线程会被阻塞等待。
2. 作用范围与用法
Synchronized
主要用在两个地方:
-
a) 同步实例方法
public synchronized void myMethod() { // 访问或修改 this 对象实例的状态 }
- 锁对象: 当前实例对象 (
this
)。 - 效果: 当线程 A 调用
obj1.myMethod()
时,它会尝试获取obj1
的锁。如果锁可用,线程 A 获得锁并执行方法体;此时,任何其他线程试图调用obj1
的任何一个同步实例方法都会被阻塞(因为它们都需要obj1
的锁)。但调用obj2.myMethod()
或obj1
的非同步方法不受影响(因为它们不竞争obj1
的锁)。
- 锁对象: 当前实例对象 (
-
b) 同步静态方法
public static synchronized void myStaticMethod() { // 访问或修改类的静态状态 }
- 锁对象: 当前类的
Class
对象 (例如MyClass.class
)。 - 效果: 当线程 A 调用
MyClass.myStaticMethod()
时,它会尝试获取MyClass.class
对象的锁。此时,任何其他线程试图调用MyClass
的任何一个同步静态方法都会被阻塞(因为它们都需要MyClass.class
的锁)。对实例方法和非静态同步方法无影响。
- 锁对象: 当前类的
-
c) 同步代码块
public void myMethod() { // 非同步代码... synchronized (lockObject) { // lockObject 是任意对象引用 // 需要同步保护的临界区代码 // 访问或修改共享状态 } // 非同步代码... }
- 锁对象: 显式指定的任意对象 (
lockObject
)。这个对象的选择至关重要,它定义了锁的粒度。 - 效果: 线程进入
synchronized
块前,必须获得lockObject
的锁。离开块(无论正常退出还是异常退出)时会自动释放锁。 - 优势: 提供了更精细的控制粒度,可以只同步真正需要互斥访问的部分代码,而不是整个方法,减少锁竞争,提高并发性能。锁对象的选择也更加灵活。
- 锁对象: 显式指定的任意对象 (
3. 核心特性与原理
- a) 内置锁 / Monitor Lock:
synchronized
在 Java 中是基于 内置锁 (Intrinsic Lock) 或 监视器锁 (Monitor Lock) 实现的。每个 Java 对象都有一个与之关联的内置锁(也称为monitor
)。 - b) 互斥性 (Mutual Exclusion): 这是
synchronized
最核心的特性,确保同一时间只有一个线程持有特定对象的锁。 - c) 可重入性 (Reentrancy): 这是关键特性。一个线程可以再次获得它已经持有的锁。 这防止了线程自身造成死锁。
public class ReentrantExample { public synchronized void methodA() { methodB(); // 同一个线程可以再次获取 this 锁 } public synchronized void methodB() { // ... } }
- 内部维护一个计数器和一个持有线程标识。第一次获取锁时计数器置为1,同一线程每次重入计数器加1,退出同步块时计数器减1。当计数器归零时,锁才真正被释放。
- d) 内存可见性 (Memory Visibility):
synchronized
不仅提供互斥,还保证了内存可见性。这是通过 Java 内存模型 (JMM) 的Happens-Before
规则实现的:- 解锁 Happens-Before 于后续的加锁: 一个线程在释放锁之前对共享变量所做的修改,对于之后成功获得同一把锁的另一个线程是立即可见的。这确保了在同步块内读取到的数据是最新值,修改后的值也能被其他进入同步块的线程看到。
- 这解决了 CPU 缓存、编译器指令重排序等导致的内存不一致性问题。
4. 底层实现 (JVM 层面)
- 字节码层面:
- 对于同步方法:JVM 通过方法访问标志
ACC_SYNCHRONIZED
来标识。 - 对于同步代码块:使用
monitorenter
和monitorexit
两条字节码指令包裹临界区代码。monitorenter
尝试获取锁,monitorexit
释放锁。编译器会确保即使发生异常,monitorexit
也会被执行(通过异常表机制)。
- 对于同步方法:JVM 通过方法访问标志
- Monitor 对象: 每个对象关联一个 Monitor。Monitor 主要包含:
- Owner: 指向持有锁的线程。
- EntryList: 存放等待获取锁的线程(竞争锁时失败进入阻塞状态)。
- WaitSet: 存放调用了
wait()
方法而主动放弃锁并等待唤醒的线程。
- 锁优化: 现代 JVM(如 HotSpot)对
synchronized
进行了大量优化,使其在常见场景下性能不再像早期版本那样笨重:- 偏向锁 (Biased Locking): 假设锁通常由同一个线程获得。首次获取锁时记录线程 ID,后续该线程进入/退出同步块几乎无额外开销。当出现另一个线程竞争时,撤销偏向锁。
- 轻量级锁 (Lightweight Locking): 当锁处于无竞争或轻微竞争时,通过 CAS (Compare-And-Swap) 操作在用户态尝试获取锁,避免直接进入操作系统内核的阻塞状态。如果 CAS 失败,说明有竞争,升级为重量级锁。
- 重量级锁 (Heavyweight Locking / Mutex): 真正的操作系统级别的互斥量 (Mutex)。涉及用户态到内核态的切换,线程阻塞和唤醒成本高。这是锁竞争激烈时的最终状态。
- 锁粗化 (Lock Coarsening): 将临近的多个连续的
synchronized
块合并为一个更大的块,减少锁的获取/释放次数。 - 锁消除 (Lock Elimination): JIT 编译器通过逃逸分析,证明某个锁对象不可能被其他线程访问到(即对象是线程局部的),则直接移除该锁操作。
5. 使用注意事项与潜在问题
- a) 性能开销: 虽然优化了很多,但锁操作(尤其是重量级锁)仍有开销。获取/释放锁需要操作系统的介入(系统调用),线程阻塞和唤醒代价高昂。过度或不必要的同步会严重降低并发性能。
- b) 死锁 (Deadlock): 多个线程相互等待对方持有的锁,导致所有线程永久阻塞。经典死锁条件(互斥、请求与保持、不可剥夺、循环等待)在
synchronized
使用不当时很容易发生。// 经典死锁示例 Thread 1: synchronized(A) { synchronized(B) { ... } } Thread 2: synchronized(B) { synchronized(A) { ... } }
- 预防策略: 固定锁的获取顺序、使用超时机制(
synchronized
本身不支持,需用ReentrantLock.tryLock(timeout)
)、避免嵌套锁。
- 预防策略: 固定锁的获取顺序、使用超时机制(
- c) 锁粒度过粗/过细:
- 过粗: 使用一个锁保护大量无关资源(如用
this
锁整个对象),导致并发度低,性能差。 - 过细: 使用大量不同的锁,虽然提高了并发度,但增加了编程复杂性,更容易出错(如死锁)。需要根据共享资源的关联性合理选择锁对象。
- 过粗: 使用一个锁保护大量无关资源(如用
- d) 活跃性问题:
- 活锁 (Livelock): 线程不断响应对方动作而无法继续执行(如两个线程都礼貌地让对方先走)。
- 饥饿 (Starvation): 某些线程长时间无法获得所需资源(如 CPU 时间片或锁),导致无法执行。
- e) 不适用于所有场景: 对于复杂的同步需求(如尝试获取锁、可中断的锁获取、公平锁、等待通知的多个条件队列),
synchronized
提供的功能相对基础。这时java.util.concurrent.locks
包中的ReentrantLock
和Condition
提供了更强大灵活的控制。
6. 最佳实践
- 最小化临界区: 只同步真正访问共享资源的代码,尽快释放锁。
- 谨慎选择锁对象:
- 保护实例状态 -> 使用
this
或专门的私有实例锁对象 (private final Object lock = new Object();
)。 - 保护静态状态 -> 使用类的
Class
对象或专门的私有静态锁对象 (private static final Object STATIC_LOCK = new Object();
)。 - 避免使用
String
字面量或基本类型包装类(如Integer
)作为锁对象! 因为它们可能被 JVM 缓存/复用,导致意外的锁共享。
- 保护实例状态 -> 使用
- 避免嵌套锁: 如果必须嵌套,严格遵循全局一致的锁获取顺序。
- 优先使用同步代码块: 通常比同步方法提供更细粒度的控制。
- 考虑替代方案: 对于高并发场景或复杂需求,优先考虑
java.util.concurrent
包中的高级并发工具:ReentrantLock
(可中断、超时、公平锁、多个条件变量)ReadWriteLock
(读写分离)ConcurrentHashMap
,CopyOnWriteArrayList
等线程安全容器AtomicInteger
,AtomicReference
等原子变量类 (CAS 无锁)CountDownLatch
,CyclicBarrier
,Semaphore
,Exchanger
等同步器
- 清晰注释: 对使用
synchronized
的地方进行清晰注释,说明锁保护的是什么资源。
总结
synchronized
是 Java 语言内置的、用于实现线程同步和保证共享数据一致性的关键机制。它通过对象的内置锁提供互斥性和内存可见性,并具有可重入性。理解其工作原理(Monitor、锁优化)、作用范围(方法、代码块、锁对象)、核心特性以及潜在问题(性能、死锁、粒度)对于编写正确且高效的多线程 Java 程序至关重要。在简单场景下,synchronized
是便捷的选择,但在复杂或高性能要求的并发场景中,应积极考虑 java.util.concurrent
包提供的更高级工具。