美文网首页
什么是可重入性?ReentrantLock和Synchroniz

什么是可重入性?ReentrantLock和Synchroniz

作者: HRADPX | 来源:发表于2019-07-23 20:43 被阅读0次

      众所周知,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和Synchroniz

          本文链接:https://www.haomeiwen.com/subject/izzalctx.html