一、synchronized
和 ReentrantLock
实现原理
在 JDK-1.5 之前,共享对象的协调机制只有 synchronized
和 volatile
。 在 JDK-1.5 中增加了新的机制 ReentrantLock
。 该机制的诞生并不是为了替代 synchronized
,而是在 synchronized
不适用的情况下,提供一种可以选择的高级功能。
1. synchronized
悲观锁
通过 JVM
隐式实现的,synchronized
只允许同一时刻只有一个线程操作资源。 在 Java
中每个对象都隐式包含一个 monitor (监视器对象) 加锁的过程,其实就是竞争 monitor
的过程。
当线程进入字节码 monitorenter
指令之后,线程将持有 monitor
对象。 执行 monitorexit
时,释放 monitor 对象。 当其他线程没有拿到 monitor
对象时,则需要阻塞等待获取该对象。
2. ReentrantLock
ReentrantLock 是 Lock 的默认实现方式之一。
它是基于 AQS
(Abstract Queued Synchronizer,队列同步器)实现的。 它默认是通过 非公平锁 实现的,在它的内部有一个 state
的状态字段,用于表示锁是否被占用。
如果是 0 则表示锁未被占用,此时线程就可以把 state
改为 1,并成功获得锁。 而其他未获得锁的线程只能去排队等待获取锁资源。
synchronized 和 ReentrantLock 都提供了锁的功能。
具备 互斥性 和 不可见性 。 在 JDK-1.5 中 synchronized
的性能远远低于 ReentrantLock
。 但在 JDK-1.6 之后,synchronized
的性能略低于 ReentrantLock
。
二、synchronized
和 ReentrantLock
区别
synchronized
是 JVM
隐式实现的,而 ReentrantLock
是 Java
语言提供的 API。ReentrantLock
可设置为公平锁,而 synchronized
却不行。ReentrantLock
只能修饰代码块,而 synchronized
可以用于修饰方法、修饰代码块等。ReentrantLock
需要手动加锁和释放锁 ,如果忘记释放锁,则会造成资源被永久占用,而 synchronized
无需手动释放锁ReentrantLock
可以知道是否成功获得了锁,而 synchronized
却不行。
三、知识扩展
1. ReentrantLock
源码
无参的构造函数,创建了一个非公平锁。 用户也可以根据第二个构造函数,设置一个 boolean
类型的值,来决定是否使用公平锁来实现线程的调度。
private final Sync sync;
public ReentrantLock ( ) {
sync = new NonfairSync ( ) ;
}
public ReentrantLock ( boolean fair) {
sync = fair ? new FairSync ( ) : new NonfairSync ( ) ;
}
1.1 公平锁 VS 非公平锁
公平锁 的含义是线程需要按照请求的顺序来获得锁。而 非公平锁 则允许"插队 "的情况存在。
所谓的 插队 指的是,线程在发送请求的同时,该锁的状态恰好变成了可用。 那么此线程就可以跳过队列中所有排队的线程直接拥有该锁。
公平锁 由于有挂起和恢复所以存在一定的开销。因此性能不如 非公平锁 。
所以 ReentrantLock
和 synchronized
默认都是非公平锁的实现方式。
ReentrantLock
是通过 lock()
来获取锁,并通过 unlock()
释放锁。
2. lock()
获取锁
ReentrantLock
中的 lock()
是通过 sync.lock()
实现的。但 Sync
类中的 lock()
是一个抽象方法,需要子类 NonfairSync
或 FairSync
去实现。
@Test
public void test2 ( ) {
Lock lock = new ReentrantLock ( ) ;
try {
lock. lock ( ) ;
} finally {
lock. unlock ( ) ;
}
}
2.1 NonfairSync.lock()
非公平锁源码
final void lock ( ) {
if ( compareAndSetState ( 0 , 1 ) )
setExclusiveOwnerThread ( Thread . currentThread ( ) ) ;
else
acquire ( 1 ) ;
}
2.2 FairSync.lock()
公平锁源码
final void lock ( ) {
acquire ( 1 ) ;
}
可以看出 非公平锁 比 公平锁 只是多了一行 compareAndSetState()
方法。
该方法是尝试将 state
值由 0 置换为 1。 如果设置成功的话,则说明当前没有其他线程持有该锁,不用再去排队了,可直接占用该锁。 否则,则需要通过 acquire()
方法去排队。
2.3 acquire()
排队获取锁
public final void acquire ( int arg) {
if ( ! tryAcquire ( arg) &&
acquireQueued ( addWaiter ( Node . EXCLUSIVE) , arg) )
selfInterrupt ( ) ;
}
2.4 NonfairSync.tryAcquire()
非公平锁尝试获取锁
如果获取锁失败,则把它加入到阻塞队列中。
protected final boolean tryAcquire ( int acquires) {
return nonfairTryAcquire ( acquires) ;
}
2.5 FairSync.tryAcquire()
公平锁尝试获取锁
protected final boolean tryAcquire ( int acquires) {
final Thread current = Thread . currentThread ( ) ;
int c = getState ( ) ;
if ( c == 0 ) {
if ( ! hasQueuedPredecessors ( ) &&
compareAndSetState ( 0 , acquires) ) {
setExclusiveOwnerThread ( current) ;
return true ;
}
}
else if ( current == getExclusiveOwnerThread ( ) ) {
int nextc = c + acquires;
if ( nextc < 0 )
throw new Error ( "Maximum lock count exceeded" ) ;
setState ( nextc) ;
return true ;
}
return false ;
}
对于此方法来说,公平锁 比 非公平锁 只多一行代码 !hasQueuedPredecessors()
。
它用来查看队列中,是否有比它等待时间更久的线程。 如果没有,就尝试一下是否能获取到锁。 如果获取成功,则标记为已经被占用。 如果获取锁失败,则调用 addWaiter()
方法,把线程包装成 Node
对象,同时放入到队列中。 但 addWaiter
方法并不会尝试获取锁,acquireQueued()
方法才会尝试获取锁。 如果获取失败,则此节点会被挂起。
2.6 acquireQueued()
源码
final boolean acquireQueued ( final Node node, int arg) {
boolean failed = true ;
try {
boolean interrupted = false ;
for ( ; ; ) {
final Node p = node. predecessor ( ) ;
if ( p == head && tryAcquire ( arg) ) {
setHead ( node) ;
p. next = null ;
failed = false ;
return interrupted;
}
if ( shouldParkAfterFailedAcquire ( p, node) &&
parkAndCheckInterrupt ( ) )
interrupted = true ;
}
} finally {
if ( failed)
cancelAcquire ( node) ;
}
}
该方法会使用 for(;;)
无限循环的方式来尝试获取锁。
若获取失败,则调用 shouldParkAfterFailedAcquire()
方法,尝试挂起当前线程。
2.7 shouldParkAfterFailedAcquire()
源码
* *
* AbstractOwnableSynchronizer
* 判断线程是否可以被挂起
* /
private static boolean shouldParkAfterFailedAcquire ( Node pred, Node node) {
int ws = pred. waitStatus;
if ( ws == Node . SIGNAL)
return true ;
if ( ws > 0 ) {
do {
node. prev = pred = pred. prev;
} while ( pred. waitStatus > 0 ) ;
pred. next = node;
} else {
compareAndSetWaitStatus ( pred, ws, Node . SIGNAL) ;
}
return false ;
}
前驱节点 的状态为 SIGNAL 。
SIGNAL 状态的含义是 后继节点 处于 等待 状态,当前节点释放锁后将会唤醒 后继节点 。
所以在上面这段代码中,会先判断 前驱节点 的状态。
如果为 SIGNAL ,则当前线程可以被挂起并返回 true 。 如果前驱节点 的状态 > 0,则表示 前驱节点 取消了,这时候需要一直往前找,直到找到最近一个正常等待的 前驱节点 。 然后把它作为自己的 前驱节点 ;如果前驱节点正常(未取消),则修改 前驱节点 状态为 SIGNAL 。
到这里整个加锁的流程就已经走完了。 最后的情况是,没有拿到锁的线程会在队列中被挂起,直到拥有锁的线程释放锁之后,才会去唤醒其他的线程去获取锁资源。
整个运行流程如下图所示:
3. unlock()
释放锁
public void unlock ( ) {
sync. release ( 1 ) ;
}
public final boolean release ( int arg) {
if ( tryRelease ( arg) ) {
Node h = head;
if ( h != null && h. waitStatus != 0 )
unparkSuccessor ( h) ;
return true ;
}
return false ;
}
先调用 tryRelease()
方法尝试释放锁。 如果释放成功,则查看头结点的状态是否为 SIGNAL 。 如果是,则唤醒头结点的下个节点关联的线程。 如果释放锁失败,则返回 false
。
3.1 tryRelease()
源码
protected final boolean tryRelease ( int releases) {
int c = getState ( ) - releases;
if ( Thread . currentThread ( ) != getExclusiveOwnerThread ( ) )
throw new IllegalMonitorStateException ( ) ;
boolean free = false ;
if ( c == 0 ) {
free = true ;
setExclusiveOwnerThread ( null ) ;
}
setState ( c) ;
return free;
}
会先判断当前的线程是不是占用锁的线程。
如果不是的话,则会抛出异常。 如果是的话,则先计算锁的状态值 getState() - releases
是否为 0。 如果为 0,则表示可以正常的释放锁,然后清空独占的线程,最后会更新锁的状态并返回执行结果。
4. JDK-1.6 锁优化
4.1 自适应自旋锁
JDK-1.5 在升级为 JDK-1.6 时,HotSpot
虚拟机团队在锁的优化上下了很大功夫。 比如实现了 自适应式自旋锁 、锁升级 等。
JDK-1.6 引入了自适应式自旋锁 ,意味着自旋的时间不再是固定的时间了。
比如在同一个锁对象上,如果通过自旋等待成功获取了锁。 那么虚拟机就会认为,它下一次很有可能也会成功(通过自旋获取到锁) 因此允许自旋等待的时间会相对的比较长。 而当某个锁通过自旋很少成功获得过锁,那么以后在获取该锁时,可能会直接忽略掉自旋的过程,以避免浪费 CPU 的资源,这就是 自适应自旋锁 的功能。
4.2 锁升级
锁升级 其实就是从 偏向锁 到 轻量级锁 再到 重量级锁 升级的过程。这是 JDK-1.6 提供的优化功能,也称之为 锁膨胀 。
4.3 偏向锁
偏向锁 是指在无竞争的情况下设置的一种锁状态。偏向锁 的意思是,它会偏向于第一个获取它的线程。
当锁对象第一次被获取到之后,会在此对象头中设置标示为 “01”,表示偏向锁的模式,并且在对象头中记录此线程的 ID。 这种情况下,如果是持有偏向锁的线程每次在进入的话,不再进行任何同步操作。 如 Locking 、Unlocking 等,直到另一个线程尝试获取此锁的时候,偏向锁模式才会结束。
但如果在多数锁总会被不同的线程访问时,偏向锁模式就比较多余了。 此时可以通过 -XX:-UseBiasedLocking
来 禁用偏向锁 以提高性能。
4.4 轻量锁 和 重量锁
在 JDK-1.6 之前,synchronized
是通过操作系统的互斥量(mutex lock
)来实现的。 这种实现方式,需要在用户态和核心态之间做转换,有很大的性能消耗。 这种传统实现锁的方式被称之为 重量锁 。
而 轻量锁 是通过比较并交换(CAS,Compare and Swap)来实现的。
它对比的是线程和对象的 Mark Word
(对象头中的一个区域)
如果更新成功,则表示当前线程成功拥有此锁。 如果失败,虚拟机会先检查对象的 Mark Word
是否指向当前线程的栈帧。 如果是,则说明当前线程已经拥有此锁,否则,则说明此锁已经被其他线程占用了。
当两个以上的线程争抢此锁时,轻量级锁 就膨胀为 重量级锁 ,这就是 锁升级 的过程,也是 JDK 1.6 锁优化的内容。
四、小结
首先是 synchronized
和 ReentrantLock
的实现过程。 然后是 synchronized
和 ReentrantLock
的区别。 最后通过源码的方式,看了 ReentrantLock
加锁和解锁的执行流程。 接着又看了 JDK-1.6 中的锁优化,包括自适应式自旋锁的实现过程,以及 synchronized
的三种锁状态和锁升级的执行流程。
synchronized
刚开始为偏向锁,随着锁竞争越来越激烈,会升级为轻量级锁和重量级锁。如果大多数锁被不同的线程所争抢就不建议使用偏向锁了。
1. ReentrantLock 的具体实现细节是什么?
2. JDK-1.6 时锁做了哪些优化?