引言
在Java并发编程的世界中,保证线程安全是核心挑战之一。为了实现这一目标,我们有两种主要的锁机制:由语言原生支持的synchronized关键字和java.util.concurrent.locks.Lock接口及其实现类(如ReentrantLock)。尽管二者的最终目的相同——确保多线程环境下对共享资源的安全访问,但它们在实现原理、特性、灵活性以及性能表现上存在着显著差异。本文将深入剖析synchronized与Lock的内部原理,并对它们的性能进行对比,以帮助开发者在实际应用中做出最合适的选择。
Synchronized的深度剖析
Synchronized是Java提供的一种内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。它的使用非常简单,可以用于同步代码块或同步方法。
实现原理与锁升级
Synchronized的实现严重依赖于JVM和对象头中的标记字(Mark Word)。在Java对象的头信息中,有一部分用于存储对象的运行时数据,其中就包括锁状态。为了优化性能,JVM引入了锁升级机制,该过程是不可逆的:
1. 无锁状态: 对象刚被创建时的状态。
2. 偏向锁: 当第一个线程访问同步块时,JVM会将对象头中的锁标志位设置为偏向模式,并记录下该线程的ID。在这个线程后续的访问中,无需进行任何同步操作(如CAS),直接检查线程ID即可,极大提升了单线程重复加锁的性能。
3. 轻量级锁: 当有第二个线程尝试获取锁时,偏向锁会升级为轻量级锁。该锁通过CAS操作来竞争锁标志位。若竞争成功,当前线程获得锁;若失败,表示存在竞争,线程会通过自旋(循环尝试)的方式等待锁释放。自旋避免了线程被挂起和唤醒的开销,适用于锁占用时间很短的场景。
4. 重量级锁: 如果轻量级锁竞争激烈,或者线程自旋超过一定次数仍未获得锁,锁将升级为重量级锁。此时,未获得锁的线程会被挂起(进入阻塞状态),并放入对象的等待队列中,等待持有锁的线程释放锁后将其唤醒。这个过程中会涉及到操作系统内核态的互斥量(Mutex)操作,导致用户态和内核态之间的切换,开销最大。
特点与局限性
Synchronized的主要特点是使用简便,由JVM负责加锁和释放锁,能有效避免死锁(在正常编码情况下)。然而,它也存在一些局限性:锁的获取和释放是固化的(在进入和退出同步块时自动进行);无法中断一个正在等待锁的线程;无法设置获取锁的超时时间;等待条件单一(每个锁仅有一个隐式的等待队列)。
Lock的深度剖析
Lock是JDK 1.5之后在java.util.concurrent.locks包下提供的显式锁接口,其最常用的实现是ReentrantLock。
实现原理
ReentrantLock的实现基于抽象队列同步器(AbstractQueuedSynchronizer, AQS)。AQS内部维护了一个双向链表的同步队列(CLH队列的变体)和一个状态变量(state)。
当线程尝试获取锁时,会调用AQS的acquire方法。如果state为0(表示锁未被占用),则通过CAS操作将其设置为1,并设置当前线程为锁的持有者。如果state不为0且持有者就是当前线程(可重入),则state累加。如果锁已被其他线程占用,当前线程会被封装成一个节点(Node)并加入到同步队列的尾部,然后进入等待状态。
与synchronized的“一切交由JVM”不同,Lock的加锁、释放锁逻辑都在Java代码层面实现,提供了更大的灵活性和控制力。
高级特性
Lock接口提供了synchronized所不具备的高级功能,这正是其价值所在:
1. 可中断的锁获取: 使用lockInterruptibly()方法,在等待锁的过程中,线程可以响应中断。
2. 超时获取锁: 使用tryLock(long time, TimeUnit unit)方法,线程可以在指定的时间内尝试获取锁,超时则返回false,避免了无限期等待。
3. 公平锁与非公平锁: ReentrantLock的构造函数允许选择创建公平锁或非公平锁。公平锁按照线程请求的顺序分配锁,保证了公平性但可能降低吞吐量;非公平锁允许“插队”,吞吐量通常更高,但可能导致某些线程饥饿。Synchronized则是非公平的。
4. 多条件变量(Condition): 一个Lock对象可以关联多个Condition实例,用于实现更精细的线程等待/通知机制。而synchronized只能通过wait()/notify()操作一个等待集。
性能对比与选型建议
在早期的JDK版本中,synchronized因为其重量级锁的实现,性能远逊于ReentrantLock。但随着JVM的不断优化,尤其是引入了偏向锁和轻量级锁之后,synchronized在低竞争场景下的性能已经得到了极大的提升,甚至在某些情况下优于Lock。
性能考量
在低竞争或无竞争的情况下,synchronized的偏向锁机制优势明显,因为它避免了CAS操作的开销。而在高竞争场景下,当锁升级为重量级锁后,其性能与ReentrantLock的非公平模式相差不大。但ReentrantLock提供了更灵活的控制手段,例如可以通过tryLock()避免线程直接进入阻塞状态,或者使用公平锁策略来满足特定需求。
选型建议
优先使用synchronized的情况:
1. 同步逻辑简单,不需要Lock提供的高级功能(如超时、中断、条件变量等)。
2. 锁的竞争程度不高,大部分情况下是单线程重复进入同步块,偏向锁能发挥优势。
3. 希望代码更简洁,由JVM自动管理锁的释放,减少编码错误。
考虑使用Lock的情况:
1. 需要尝试非阻塞地获取锁(tryLock),或可中断、超时获取锁。
2. 需要多个条件变量来实现复杂的线程协作(例如生产者-消费者模型)。
3. 需要公平锁机制来保证线程获取锁的公平性。
4. 在高竞争环境下,需要通过更精细的控制来优化性能。
总结
Synchronized和Lock是Java并发编程中两大核心的同步工具。Synchronized以其简洁性和JVM层面的持续优化,在大多数常规场景下是首选。而Lock则凭借其丰富的API和极高的灵活性,在需要复杂同步控制和高性能调优的场景中发挥着不可替代的作用。作为开发者,理解二者的底层原理和特性差异,是写出高效、健壮并发代码的关键。在实际项目中,应根据具体需求,权衡易用性、功能性和性能,做出最合适的技术选型。
466

被折叠的 条评论
为什么被折叠?



