• 【JDK1.8】JUC——ReentrantLock


    一、前言

    在之前的几篇中,我们回顾了锁框架中比较重要的几个类,他们为实现同步提供了基础支持,从现在开始到后面,就开始利用之前的几个类来进行各种锁的具体实现。今天来一起看下ReentrantLock,首先来看一下Java doc 上对ReentrantLock的解释:

    ReentrantLock,作为可重入的互斥锁,具有与使用synchronized方法和语句相同的基本行为和语义,但功能更强大。

    对上面这句话的解释:

    1. 拥有和synchronized关键字一样的行为,可重入互斥(注意,synchronized也是可重入的)
    2. 更强大的功能:比如支持公平锁和非公平锁,前面文章提到过的Condition的使用等。

    另外来看一下它的最佳时间:

    class X {
        private final ReentrantLock lock = new ReentrantLock();
        public void m() {
            // 上锁
            lock.lock();
            try {
                // 执行方法体
            } finally {
                lock.unlock()
            }
        }
    }
    

    要点就是try-finally,在执行的最后,无论是否出错都调用unlock解锁,保证释放资源。


    二、源码分析

    2.1、成员变量

    早在第一章JUC.Lock综述的时候,我们就大体看过juc包里的关系图,上面提到过,ReentrantLock支持公平锁和非公平锁,其原因就是内部实现了两个内部类FairSyncNonfairSync,分别实现了对应的支持,先来看一下成员变量:

    public class ReentrantLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = 7373984872572414699L;
        private final Sync sync;
        
        public ReentrantLock() {
            sync = new NonfairSync();
        }
    
        /**
         * fair = true:公平锁, false:非公平锁
         */
        public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    }
    

    这里由于final关键字,方便理解,直接将构造方法也一并放了进来。

    • 默认构造器在初始化的时候,实例化的是非公平锁,举个栗子, 我去买蛋糕,蛋糕店刚好出炉了一个蛋糕,我刚好碰到买到了,那我就买回去了。
    • 而带fair的构造器,为true的时候,实例化的是公平锁,再举个栗子,我去买蛋糕,蛋糕店刚好又出炉了一个,我想买,但是人家已经预定好了,要买就得排队等。

    2.2、内部类

    前面提到的FairSyncNonfairSync都继承自ReentrantLock的内部类,而在JUC关系图中,Sync在大部分的锁框架中都各自进行了不同的实现,但是都继承自AQS。

    2.2.1、Sync

    一起来看一下ReentrantLock中的Sync实现:

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
    
        abstract void lock();
        // 尝试获取非公平锁
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // AQS中的state成员变量,0表示没有线程持有锁
            int c = getState();
            if (c == 0) {
                // cas设置入锁次数,仅尝试一次,成功则设置当前线程为独占线程
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果当前线程为独占线程,则重入次数叠加
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 否则尝试获取锁失败
            return false;
        }
    	// 释放锁
        protected final boolean tryRelease(int releases) {
            // 由于可重入性,所以获取当前重入次数,与releases相减
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 为0则说明已经全部释放,则清空持有状态
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
        ...
    }
    

    lock()的逻辑由继承的NonfairSyncFairSync自己实现。

    这里笔者阅读的时候注意到一个问题:前面提到FairSync是公平锁,每个线程按照队列的顺序来获取,但是其父类却有nonfairTryAcquire()方法来尝试直接获取锁,这一实现放在NonfairSync中不是更合适吗?为什么要放在父类中呢?

    仔细查看代码后发现,ReentrantLock里有tryLock()方法:允许线程尝试获取一次锁,有则获得锁,返回true,没有则返回false。

    那么就可以解释的通了,因为这个是ReentrantLock的public方法,所以不论是公平锁还是非公平锁,都可以调用,所以说,nonfairTryAcquire()方法放在的父类Sync当中。


    2.2.2、NonfairSync

    下面是非公平锁的实现:

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
    
        final void lock() {
            // cas 原子性操作将state设置为1,如果设置成功,则说明当前没有线程持有锁
            if (compareAndSetState(0, 1))
                //把当前线程设置为独占锁
                setExclusiveOwnerThread(Thread.currentThread());
            // 反之则锁已经被占用,或者set失败
            else
                // 调用父类AQS分析里提到过的方法,以独占模式获取对象,acquire会调用tryAcquire
                acquire(1);
        }
    
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    

    其实非公平锁核心实现在上一篇AQS之中就基本分析过了,所以这里的代码就相对简单。

    但是为什么lock()方法不直接调用acquire(1),而是直接先尝试CAS操作设置呢,笔者暂时没有想明白,因为调用acquire(1)后,会进入tryAcquire(1),里面的操作其实是一样的,估计就是为了更快尝试获取?


    2.2.3、FairSync

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
    
        final void lock() {
            acquire(1);
        }
    
        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;
        }
    }
    

    NonfairSync不同,公平锁的tryAcquire中,当发现当前没有线程持有锁的时候,会判断队列中有无前驱节点,之所以要判断的原因是:

    在当前持有锁的线程调用unlock()的的过程中,存在的这样一个过程:

    tryRelease()到唤醒后继节点的过程中,可能有新的线程进来,这个时候,就需要判断队列是否有其他节点等待了,这就是公平锁的奥义吧。

    详情查看hasQueuedPredecessors代码,查看当前线程前有没有前驱节点:

    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    

    其中代码值得我们认真思考一下:

    1. 为什么要先从tail开始赋值?
    2. 说明时候h.next为null

    这两点,我们要结合入队列时候的代码说起,在前面结束AQS的时候,分析过enq()方法:

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 队列初始化
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            // 重复执行插入直到return
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    

    我们先假设从head开始赋值:

    当第一个线程调用enq的时候,cpu切换,进入了线程t2的hasQueuedPredecessors, 首先对head进行赋值,此时还没有到compareAndSetHead(new Node()),那么此时 head = null,这个时候cpu切换,t1继续执行,执行完了tail == head,再切换回t2,继续执行Node t = tail;,这个时候,在return的时候,h != t成立,当调用(s = h.next) == null,h为null,报了空指针。

    所以先从tail开始赋值,至少能保证在tail有值的时候,head必然有值!

    另外什么时候h.next == null,其实可以从enq的else里找到答案,也是第一次enq插入空队列的时候,当线程执行到compareAndSetTail(t, node)的时候,head != tail,但是此时head.next还未开始赋值,所以为null。


    三、总结

    关于ReentrantLock的使用例子,其实在第一篇将Lock的时候,就曾经有提到过,是Java Doc上提供的一个例子,典型的生产者-消费者模式,这里笔者就不赘述了。其实ReentrantLock关键的核心实现在于AQS,AQS仔细阅读的话,还是有很多值得推敲的地方,再一次觉得它的实现博大精深~最后谢谢各位园友观看,如果有描述不对的地方欢迎指正,与大家共同进步!

  • 相关阅读:
    完整安装always on 集群——转自菜鸟就是我
    markdown文件的基本常用编写语法-转自凌云之翼
    python基础学习DAY5——转自金角大王
    python基础学习DAY4——转自金角大王
    python函数返回局部变量,局部&全局变量同名问题
    python2.7和python3.6共存,使用pip安装第三方库
    理解if __name__ == '__main__':
    pycharm中查看快速帮助和python官方帮助文档
    python第三方库PIL安装的各种坑
    python2.7安装第三方库错误:UnicodeDecodeError: 'ascii' codec can't decode byte 0xcb in position 0
  • 原文地址:https://www.cnblogs.com/joemsu/p/9460156.html
Copyright © 2020-2023  润新知