程序员Android开发

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

2021-02-18  本文已影响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............

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

结果是:死锁!

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

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

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

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

我们修改上面的例子,为了不影响之前的 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及其子类,需要实现三个方法:

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 之前,利用尽可能的『机会』去尝试获取锁:

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 ,并不影响核心流程;因为锁的释放比较简单,因此我就不在过多分析了,大家自己看一下就清楚了。

上一篇 下一篇

猜你喜欢

热点阅读