Java并发--ReentrantLock实现分析

ReentrantLock是基于AQS实现的可重入独占锁,如果还不了解AQS实现原理的同学可以先去AQS原理分析学习一哈。

如果文章中由任何不妥或者谬误之处,请批评指正。

公平锁or非公平锁

ReentrantLock核心功能的实现,依赖于继承AQS类实现的同步器。

ReentrantLock有三个核心内部类,基于AQS实现的Sync(同步器类),基于Sync类实现的NonfairSync(非公平同步器)和FairSync(公平同步器)。

公平锁调用了FairSync类的方法,公平锁的含义是,多个线程依照依次尝试去获取锁,而锁释放以后,等待线程按照先后顺序获取锁。

非公平锁的实现就是调用了NonfairSync类的方法,非公平锁的含义就是,多个线程有时间顺序的去尝试获取锁,如果获取失败进入等待状态,如果共享资源得到释放,这些等待线程再去竞争获取锁(和尝试获取锁的先后顺序无关),所以是非公平的。

举个通俗的例子就是,一群人去吃烤鸭,店主每次只能烤出一只烤鸭给客户吃,公平锁就是等着吃烤鸭的那些人,排起了长队,而非公平锁就是,等着的那群人不仅不排队,还要互相推搡,争抢着去吃烤鸭。

1. 非公平锁

先讲非公平锁的原因是非公平锁比较常用。ReentrantLock无参的构造方法就会生成一个非公平锁。

    public ReentrantLock() {
        // 默认生成的是非公平同步器
        sync = new NonfairSync();
    }

1.1 非公平锁的获取

我们来直接看一下NonfairSync的源码。

    static final class NonfairSync extends Sync {
        // 序列化ID
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            // 尝试快速获取锁
            // 如果当前State状态码是0,说明没有线程持有锁
            if (compareAndSetState(0, 1)) 
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1); // AQS中的方法
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

我们重点来看快速获取锁失败以后的操作。调用了acquire(int)方法,而根据AQS的源码,我们已经了解到,acquire(int)方法会调用 tryAcquire(int)方法,饶了一圈又回来了,眼皮子底下的tryAcquire(int)其实是个皮包公司。

最终还是nonfairTryAcquire(int)方法真实实现了非公平锁。

nonfairTryAcquire(int)又在哪呢?在NonfairSync父类里!这里不得不吐槽一句了,这种写法十分不符合OO设计原则,估计是Doug Lea大神一开始压根就没想公平锁这个东西,所以就顺手写下来了,至今还没有人敢动过这些代码,所以可见Doug Lea大神不是吾等小辈可以评价的。

我们来看一下nonfairTryAcquire(int)是怎么设计的。

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread(); // 获取当前线程
            int c = getState(); // 获取AQS中的state状态码
            if (c == 0) { // state为0则说明没有线程持有锁
                if (compareAndSetState(0, acquires)) {
                    // CAS成功就直接将当前线程设置为共享资源的独占线程
                    setExclusiveOwnerThread(current); 
                    return true;
                }
            }
            // 下面是重入锁的情况
            else if (current == getExclusiveOwnerThread()) {
                // 重入锁就让state加上申请次数(这里都是1)
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc); // 重入一次,state就加一
                return true;
            }
            // 如果不能获取锁就返回false
            return false;
        }

这段代码还是很容易理解的,而且逻辑也比较清晰,可以看到重入锁的设计就是使用了AQS中的state状态码呀。

    // AQS中的同步状态码就是这样婶儿滴
    private volatile int state;

ReentrantLock核心设计思路还是CAS+volatile的机制,确保了线程安全,而通过只使用state状态码来标识线程状态,比起使用操作系统的锁机制需要上下文的切换,可以说是非常的节省资源了。(顺便一提,目前为止安卓虚拟机上ReentrantLock的效率比使用synchronized关键字的效率要高的多)。

如果锁获取失败,那么就进入了我们熟悉的AQS流程。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

无论是addWaiter(Node.EXCLUSIVE)将当前线程包装成一个队列节点,然后入队,还是 acquireQueued(Node,arg)方法让线程等待,并且等待唤醒以后竞争资源,这都是AQS为我们实现的方法,在此不做详细解释。

2. 公平锁

公平锁比起非公平锁效率低一些,因为非公平锁有一些快速获取锁的机制,但是这种机制会导致插队的情况发生,而如果没有这些机制,就会需要等待队列中的线程的被唤醒,这个过程由操作系统来安排,所以速度会慢一些。

2.1 公平锁的获取

我们直接来看FairSync类的实现。

    static final class FairSync extends Sync {
        // 序列化ID
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            // 没有了快速获取锁的if语句
            acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); // 获取AQS同步状态码
            if (c == 0) { // 说明锁处于释放状态
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 如果这个线程在队列中没有前驱节点(是队头或者根本没有建立队列)
                    // 那么就尝试CAS操作修改同步状态码,并设置线程独占
                    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;
        }
    }

FairSync类中的lock()方法中没有快速获得锁的if语句,而是直接进入acquire(int)方法,这是第一点防止插队机制,原理是防止有解锁时,有未在队列中的新建线程,直接获得了锁。

FairSync类中的tryAcquire(int)NonfairSync类中的方法只有一处区别,就是判断当前线程在队列中是否有前驱节点,如果没有前驱节点,才能获取锁。这是第二点防止插队机制,原理是防止进入acquire(int)的线程,还没有入队,又反过头来直接获取了锁。

这两点不同于非公平锁的机制保证了,多个线程会顺序的进入等待队列,最终做到了公平锁的机制。

3. 解锁过程

这个过程相对于获取锁的过程就简单了许多。

    public void unlock() {
        // 直接调用同步器中的release(int)方法,无论是否为公平锁
        sync.release(1);
    }

release(int)是AQS中的释放资源的方法,也很简单。

    public final boolean release(int arg) {
        if (tryRelease(arg)) { // 根据子类实现的方法判断
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); // 唤醒节点内的线程
            return true;
        }
        return false;
    }

最终还是ReentrantLock中的tryRelease方法在搞事情,我们观察一下它。

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases; // 调用一次state就减少1
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) { // 当state为0时,说明可以释放锁了
                free = true;
                setExclusiveOwnerThread(null); // 设置资源线程占用为null
            }
            setState(c); // 修改state状态码
            return free; 
        }

这个方法可以说十分的容易理解了。而state的增加和减少,实现了对于同一线程的锁重入,获取成功一次state就加1,释放锁成功一次就减1,为0时则说明锁已被彻底释放


  1. ReentrantLock实现原理深入探究
  2. 深入剖析ReentrantLock公平锁与非公平锁源码实现

猜你喜欢

转载自blog.csdn.net/cringkong/article/details/80585548