众所周知,ReentrantLock和synchronized是可重入锁。下面分析什么是可重入性,以及它们如何实现可重入性
1 什么是可重入?
先看定义,定义来自维基百科
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
代码测试,两个方法的锁都是MyReent对象,在run()内部调用了function()方法,在执行run()方法时,已经拿到了锁对象reent,假设synchronized是不可重入的,那么在run()内部调用function() 时会被阻塞,但是事实却没有,所以可以看出synchronized是可重入的。即使是两个方法相互调用也不会发生死锁,只会抛出栈溢出异常。
简言之,一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明该锁是可重入的,反之则不可重入。
public class ReentrantTest {
public static void main(String[] args) {
MyReent reent = new MyReent();
new Thread(reent).start();
}
}
class MyReent implements Runnable{
@Override
public synchronized void run() {
System.out.println(Thread.currentThread().getName() + " run");
function();
}
public synchronized void function(){
System.out.println(Thread.currentThread().getName()+ " function");
}
}
Thread-0 run
Thread-0 function
2 synchronized如何实现可重入性
synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。每个锁对象内部维护一个计数器,该计数器初始值为0,表示任何线程都可以获取该锁并执行相应的方法。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
3 ReentrantLock如何实现可重入性
ReentrantLock在内部使用了内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作带来效率问题。
下面是部分ReentrantLock源码
// Sync继承于AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
// ReentrantLock默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 可以通过向构造方法中传true来实现公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
线程抢锁过程(公平锁实现):
protected final boolean tryAcquire(int acquires) {
// 当前想要获取锁的线程
final Thread current = Thread.currentThread();
// 当前锁的状态
int c = getState();
// state == 0 此时此刻没有线程持有锁
if (c == 0) {
// 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
// 看看有没有别人在队列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
// 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=
// 因为刚刚还没人的,我判断过了
compareAndSetState(0, acquires)) {
// 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
setExclusiveOwnerThread(current);
return true;
}
}
// 会进入这个else if分支,说明是重入了,需要操作:state=state+1
// 这里不存在并发问题
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁
return false;
}
从上面可以看出:
(1) 当一个线程在获取锁过程中,先判断state的值是否为0,如果是表示没有线程持有锁,就可以尝试获取锁(不一定获取成功)。
(2) 当state的值不为0时,表示锁已经被一个线程占用了,这时会做一个判断current == getExclusiveOwnerThread()(这个方法返回的是当前持有锁的线程),这个判断是看当前持有锁的线程是不是自己,如果是自己,那么将state的值加1就可以了,表示重入返回即可。
本文完
本文参考:《深入理解Java虚拟机第二版》
源码分析参考:一行一行源码分析清楚AbstractQueuedSynchronizer,写的非常好!!!
网友评论