什么是可重入性?ReentrantLock和Synchronized是怎么实现可重入的?

  众所周知,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,写的非常好!!!

你可能感兴趣的:(什么是可重入性?ReentrantLock和Synchronized是怎么实现可重入的?)