JUC:可重入锁ReentrantLock

444 阅读6分钟

前言

在上一章之前已经详细描述过AQS,ReentrantLock则是在AQS的基础上实现的,如果不了解AQS的同学可以先看看 JUC:并发基础之AQS

可重入锁,见名知意,和synchronized一样当前线程可重复获取锁。与synchronized不同的是,ReentrantLock支持更多高级功能,如可实现公平锁、非公平锁,可响应中断,以及单个锁可绑定多个条件等。

Lock接口

ReentrantLock实现了Lock接口,我们先看看Lock接口的定义。

Lock接口定义了6个方法:

  • lock():获取锁
  • lockInterruptibly():可响应中断方式获取锁
  • tryLock():尝试获取锁,失败直接返回
  • tryLock(long time, TimeUnit unit):尝试获取锁,到时间仍获取不到则返回。
  • unlock():解锁
  • newCondition():获取条件对象

ReentrantLock

类图

可以看到,ReentrantLock内有三个内部类。 分别是Sync、NonfairSync、FairSyn,其中NonfairSync、FairSyn继承自Sync,Sync类继承自AbstractQueuedSynchronizer抽象类。

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的getState方法
            int c = getState();
            // 如果资源没有被占用
            if (c == 0) {
                // 如果csa方式获取资源成功
                if (compareAndSetState(0, acquires)) {
                    // 设置当前独占式获取锁线程为当前线程
                    setExclusiveOwnerThread(current);
                    // 返回ture
                    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) {
            // 取得需要的资源
            int c = getState() - releases;
            // 如果当前线程不是独占式占有资源的线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                // 直接抛异常
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果释放资源后的资源为0
            if (c == 0) {
                // 返回释放资源成功
                free = true;
                // 设置当前独占式获取资源线程为null    
                setExclusiveOwnerThread(null);
            }
            // 设置资源数
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            // 返回当前线程是否是获取资源线程
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        // conditionObject用于等待/唤醒,AQS里有讲
        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;
        }

        /**
         * 从流里反序列化实例
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

可以看到,Sync类中主要定义了尝试非公平式获取资源的方法。以及尝试释放资源的方法。

  • 尝试非公平式获取资源(nonfairTryAcquire):
    • 判断当前资源是否被占用
      • 没有被占用则尝试获取资源
    • 判断当前资源是否被当前线程占用
      • 是则修改资源的占用数
  • 尝试释放资源(tryRelease):
    • 计算释放资源后的资源
    • 判断当前线程是否独占资源
      • 不是则抛异常
    • 如果释放资源后的资源为0则返回值为true,同时将占用资源线程设置为null
    • 设置资源

可以看到,如果该锁被获取了n次,那么前(n-1)次tryRelease方法必须返回false,而只有同步状态完全释放了,才能返回true。

NonfairSync

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * 实现了Sync的抽象方法lock
         */
        final void lock() {
            // CAS方式获取设置资源
            if (compareAndSetState(0, 1))
                // 设置成功则直接设置当前线程为独占资源线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 调用AQS的acquire方法
                acquire(1);
        }
        
        // 直接调用Sync的非公平式获取资源方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

总结一下非公平式lock:

  • 首先CAS方式尝试获取资源,抢占资源成功则设置当前独占线程为当前线程。
  • 如果失败则调用AQS的acquire方法
  • acquire方法最终会调用到Sync的nonfairTryAcquire,再尝试获取一次资源。

也就是说,非公平锁在进入到队列之前会有两次抢占机会。

FairSync

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        //lock直接调用aqs的acquire方法
        final void lock() {
            acquire(1);
        }

        /**
         * acquire方法会第一个调用的方法就是tryAcquire
         * 上章AQS有讲
         */
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取当前资源
            int c = getState();
            // 如果资源没有被占用
            if (c == 0) {
                // 与非公平锁不同的是,此处多了hasQueuedPredecessors判断。
                // 如果hasQueuedPredecessors返回为true,则标识有其他线程先于当前线程获取锁。
                // 如果没有,则CAS方式获取锁。
                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");
                // 因为获取锁线程就是当前线程,所以不需要使用CAS方式直接setsetState(nextc);
                return true;
            }
            return false;
        }
    }
  • 公平式尝试获取资源(tryAcquire):
    • 判断当前资源是否被占用
      • 如果没有被占用则判断是否有其他线程先于当前线程获取锁
      • 如果没有则CAS方式获取锁
    • 判断占用资源是否是当前线程
      • 是则修改资源的占用数

可以看到,公平锁在非公平的基础上,加上了一个hasQueuedPredecessors判断是否有其他线程先于当前线程获取锁,这一步是保证公平的关键。我们来看看怎么实现的。

    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
  • 如果h==t,则证明队列为空,无前驱节点,返回false表示没有其他线程先于当前线程。
  • 如果h!=t,则看头节点的后继节点是否为null,如果为null返回true,表示有其他线程先于当前线程。(见AQS的enq方法,compareAndSetHead(node)完成,还没执行tail = head语句时,此时tail=null,head=newNode,head.next-null)
  • 如果h!=t,则看头节点的next是否不为null,则判断是否是当前线程,如果是返回false,否则有前驱节点,返回true。

总结

在掌握了AQS后,再来分析ReentrantLock的源码,就会非常简单,因为ReentrantLock的绝大部分操作都是基于AQS类的。所以,进行分析时要找准方向,就会事半功倍。谢谢各位同学观看~