美文网首页程序员Android开发
Java小白系列(十三):重入锁(ReentrantLock)

Java小白系列(十三):重入锁(ReentrantLock)

作者: 青叶小小 | 来源:发表于2021-02-18 14:15 被阅读0次

    一、前言

    我们上一篇分析了 AQS 《小白十二》,重点讲了获取锁和释放锁的流程,AQS 是抽象类,本篇我们就来聊聊 AQS 的子类:重入锁。再正式聊重入锁前,我先提几个小问题:

    • 为什么会有重入锁?
    • 重入锁使用的场景?
    • 没有重入锁会发生什么结果?

    好了,带着这几个问题,我们开始我们的重入锁之旅。

    二、不可重入锁(自己实现)

    JDK 中我并没有找到不可重入锁的源码,因此,只好我们手动来实现一个,看看锁不可重入的结果:

    // NonReentrantLock.java
    public class NonReentrantLock {
        private final AtomicReference<Thread> reference = new AtomicReference<>();
    
        public void lock() {
            Thread thread = Thread.currentThread();
            while (!reference.compareAndSet(null, thread)); // CAS 加锁
        }
    
        public void unlock() {
            Thread thread = Thread.currentThread();
            while (!reference.compareAndSet(thread, null)); // CAS 解锁
        }
    }
    

    测试代码:

    public class Demo implements Runnable {
        private NonReentrantLock lock = new NonReentrantLock();
    
        public static void main(String[] args) {
            new Thread(new Demo()).start();
        }
    
        @Override
        public void run() {
            int res = fab(3);
            System.out.println("3! = " + res);
        }
    
        private int fab(int n) {
            System.out.println("Enter in fab............");
            lock.lock();
            try {
                if (n <= 1) {
                    return 1;
                }
    
                return n * fab(n - 1);
            } finally {
                lock.unlock();
                System.out.println("Exit in fab............");
            }
        }
    }
    

    输出结果:

    Enter in fab............
    Enter in fab............
    

    我们来分析下造成这个结果的原因:

    • 第一次调用 fab 方法(传入数值3),进入并上锁;
    • 第二次调用 fab 方法(传入数值2 = 3-1),尝试上锁,由于锁已经被持有,只能CAS原地等待;
    • 因为无法拿到锁,也就无法继续执行;

    结果是:死锁!

    同时,我们还需要注意一点:

    • Demo 中是同一个线程;
    • 同一个线程多次进入资源临界区;

    因此,不可重入锁对于任何线程,无论是同一个线程,还是不同线程,都一视同仁,没有区别对待,那么,当存在递归时,将会发生死锁;以上的例子非常简单,我们可以想办法来避免,但实际开发中,我们很难保证同一个线程不会二次进入资源临界区而发生死锁,因此,这也是我开头提的三个小问题的解答。

    三、可重入锁(自己实现)

    锁通常是针对于不同线程,对同一块资源竞争时的手段,然而,当同一线程在已持有锁的情况下,多次进入同一资源时,我们就当能够识别出来,并让其进入;但同时,我们也需要做好记录,记录持有锁的线程进入次数与退出次数,要保证进入与退出次数正好相等时,最后一次的退出才应该真正的释放锁;否则,同样也会产生死锁。

    我们修改上面的例子,为了不影响之前的 demo,我们新建个类:

    // CanReentrantLock.java
    public class CanReentrantLock {
        private static final class LockObject {
            Thread thread = null;
            int count = 0;
    
            LockObject(Thread thread) {
                this.thread = thread;
                this.count = 1;
            }
        }
    
        private final AtomicReference<LockObject> reference = new AtomicReference<>();
    
        public void lock() {
            Thread thread = Thread.currentThread();
    
            LockObject object = reference.get();
            if (object == null) {
                object = new LockObject(thread);
                while (!reference.compareAndSet(null, object));
            } else if (object.thread == thread ){
                object.count ++;
                reference.set(object);
            } else {
                while (!reference.compareAndSet(null, new LockObject(thread)));
            }
        }
    
        public void unlock() {
            Thread thread = Thread.currentThread();
    
            LockObject object = reference.get();
            if (object != null && object.thread == thread) {
                object.count --;
                if (object.count == 0) {
                    while (!reference.compareAndSet(object, null)) ;
                }
            }
        }
    }
    

    修改我们的测试类:

    将下面的这行代码
    private NonReentrantLock lock = new NonReentrantLock();
    替换成
    private CanReentrantLock lock = new CanReentrantLock();
    即可
    

    我们再次执行我们的测试用例,结果如下:

    Enter in fab............
    Enter in fab............
    Enter in fab............
    Exit in fab............
    Exit in fab............
    Exit in fab............
    3! = 6
    

    结果符合我们的预期!

    四、JDK 之 ReentrantLock

    ReentrantLock 就是 JDK 提供的重入锁,它有一个内部静态抽象类(Sync),继承于 AQS;同时,它还有两个静态实现类继承于 Sync,分别是:NonfairSync(非公平锁)和 FairSync(公平锁)。ReentrantLock 是独占模式,因此,Sync及其子类,需要实现三个方法:

    • isHeldExclusively:根据请求的线程与当前持有的线程进行比对来判断是否需要独占,这是重入的机制(Sync实现);
    • tryRelease:释放锁(Sync实现);
    • tryAcquire:获取锁(公平与非公平在此处实现有差异,后续会分析);

    4.1、构造函数

    public class ReentrantLock implements Lock, java.io.Serializable {
        public ReentrantLock() {
            sync = new NonfairSync();
        }
        
        public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    }
    

    默认构造函数使用的是非公平锁,我们也可以指定使用公平锁。

    4.2、公平锁(FairSync)

    public class ReentrantLock implements Lock, java.io.Serializable {
        static final class FairSync extends Sync {
            /**
             * 回忆一下 AQS 的获取锁流程,再结合具体的子类,整个获取锁就能串起来:
             * lock -> acquire -> tryAcquire
             */
            final void lock() {
                acquire(1);
            }
    
            protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                
                // 资源占用情况:0 = 未占用;1 = 占用
                int c = getState();
                if (c == 0) {
                    // hasQueuedPredecessors 判断 CLH 是否为空,这就体现出『公平』特性
                    // 如果 CLH 不为空那么就不会去竞争抢占锁
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        // CLH 为空,且 CAS 成功,将 owner 设置为当前线程
                        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;
            }
        }
    }
    

    公平锁比较简单,我们来看下,非公平锁的实现。

    4.3、非公平锁

    public class ReentrantLock implements Lock, java.io.Serializable {
        static final class NonfairSync extends Sync {
            // 非公平锁,在一开始 lock 时,就先 CAS 尝试一次,如果失败则调用 acquire
            final void lock() {
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
        
            // lock 失败 -> acquire -> tryAcquire
            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
        }
    }
    

    我们就目前看到,非公平锁在加入 CLH 之前,利用尽可能的『机会』去尝试获取锁:

    • lock:第一次;
    • tryAcquire:虽然我们没看到实现,但是从调用的方法名上就能看出"非公平再尝试";

    4.3.1、Sync.nonfairTryAcquire

    public class ReentrantLock implements Lock, java.io.Serializable {
        
        abstract static class Sync extends AbstractQueuedSynchronizer {
            abstract void lock();
        
            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    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;
            }
            
            ......
        }
    }
    

    请大家仔细对比一下 FairSync.tryAcquire 方法与 Sync.nonfairTryAcquire 方法,你会发现,只有一行代码的区别:

    if (c == 0) {
        if (!hasQueuedPredecessors() &&        // 公平与非公平,唯一的区别
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    

    公平锁多了一个 CLH 的判断,而非公平锁再次 CAS 尝试获取锁,而无视 CLH。

    4.4、静态内部抽象类Sync

    public class ReentrantLock implements Lock, java.io.Serializable {
        
        abstract static class Sync extends AbstractQueuedSynchronizer {
            abstract void lock();
    
            final boolean nonfairTryAcquire(int acquires) {
                ......
            }
        
            // 释放锁;判断是否有多次重入,直到计数为0,才释放锁
            protected final boolean tryRelease(int releases) {
                int c = getState() - releases;
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;
                if (c == 0) {
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                setState(c);
                return free;
            }
        
            // 独占模式,且判断是否是:重入
            protected final boolean isHeldExclusively() {
                return getExclusiveOwnerThread() == Thread.currentThread();
            }
        
            final ConditionObject newCondition() {
                return new ConditionObject();
            }
    
            final Thread getOwner() {
                return getState() == 0 ? null : getExclusiveOwnerThread();
            }
        
            final int getHoldCount() {
                return isHeldExclusively() ? getState() : 0;
            }
        
            final boolean isLocked() {
                return getState() != 0;
            }
        }
    }
    

    五、总结

    正因为我们详细分析了 AQS 的获取和释放锁的流程;再加上,我也用自己实现的一个例子来前后对比不可重入锁的后果;因此,我们学习 ReentrantLock 非常的轻松;至少 ReentrantLock 中的其它代码,仅仅只是 setter / getter ,并不影响核心流程;因为锁的释放比较简单,因此我就不在过多分析了,大家自己看一下就清楚了。

    相关文章

      网友评论

        本文标题:Java小白系列(十三):重入锁(ReentrantLock)

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