JUC 源码分析 二 ReentrantLock

上一篇文章已完整展示了等待队列的管理(添加结点、移除取消结点)、独占模式下的acquire操作、acquire中断取消、前驱如何通知后继。这些知识已足够用来实现一个可重入锁。

本篇通过java.util.concurrent.locks.ReentrantLock类的源码来分析如何实现可重入锁。

可重入锁

可重入锁就是说当线程拥有这把锁的时候,它再次请求锁是成功的;当线程释放锁时,如果持有锁的线程对锁的请求次数大于释放次数,则该线程仍然拥有锁,直到请求次数与释放次数相等时才真正释放锁。

所以可重入锁需要一个重入计数变量,初始值设为0,当成功请求锁时加1,释放锁时减1,当释放锁之后计数为0则真正释放锁。重入锁还必须持有对锁持有者的引用,用以判断是否可以重入。

锁的公平性

如果锁能够严格按照线程请求锁的先后顺序分配锁,则认为锁具有公平性;如果某一线程能在其他等待线程之前获取到锁,则认为锁不具有公平性。

ReentrantLock

ReentrantLock是JUC包里可重入的独占锁实现,它具有三个内部类:Sync、NonfairSync、FairSync,通过构造函数的参数来指定锁是否是公平的,下面是一些核心代码:

public class ReentrantLock implements Lock, java.io.Serializable {
     private final Sync sync;

     public ReentrantLock(boolean fair) {
         sync = fair ? new FairSync() : new NonfairSync();
     }

     public void lock() {
         sync.lock();
     }

     public void unlock() {
         sync.release(1);     // 这个1表示退出锁1次。
     }

     // 带超时限制的获取
  public boolean tryLock(long timeout, TimeUnit unit)
          throws InterruptedException {
      return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  }

     // 其他代码省略
}

可以看到,ReentrantLock都是把具体实现委托给内部类而不是直接继承自AbstractQueuedSynchronizer,这样的好处是用户不会看到不需要的方法,也避免了用户错误地使用AbstractQueuedSynchronizer的公开方法而导致错误。

ReentrantLock的重入计数是使用AbstractQueuedSynchronizerstate属性的,state大于0表示锁被占用、等于0表示空闲,小于0则是重入次数太多导致溢出了。

ReentrantLock.Sync

可重入锁内部实现的超类,主要实现了公平与非公平锁的共有方法,并提供了加锁操作的统一抽象:abstract void lock();,还有核心的释放锁的操作。

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    // 执行Lock.lock()方法,留给子类根据其公平性实现。
    // 子类化的最主要原因是允许非公平的快速路径。
    abstract void lock();

    // 非公平获取
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        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)
                   // 溢出。加锁次数从0开始,加锁与释放操作是对称的,
                   // 所以绝不会是小于0值,小于0只能是溢出。
                throw new Error("Maximum lock count exceeded");

            // 锁被持有的情况下,只有持有者才能更新锁保护的资源,
            // 所以这里不需要用CAS。
            setState(nextc);
            return true ;
        }
        return false ;
    }

    protected final boolean tryRelease(int releases) {
      // 先读取state是为了获得一个读屏障,owner不是volatile的。
        int c = getState() - releases;

        // 只有锁的持有者才能释放锁
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {// 锁重入计数减到0,需要真正释放锁了。
            free = true;
            setExclusiveOwnerThread( null);
        }
    // 如果c为0,写操作完成后,其他线程就会看到锁被释放了,
       // 所以setExclusiveOwnerThread必须在这个写之前完成。
        setState(c);
        return free;
    }

    protected final boolean isHeldExclusively() {
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    // Methods relayed from outer class
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }

    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0; // 以state属性作为加锁次数。
    }

    final boolean isLocked() {
        return getState() != 0;     // 加锁次数为0表示没有被拥有
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

ReentrantLock.FairSync

提供公平性的锁实现。实现公平性的关键在于:如果锁被占用且当前线程不是持有者也不是等待队列的第一个,则进入等待队列。

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

    final void lock() {
        acquire(1); //acquire会首先调用tryAcquire,所以公平策略的控制留给tryAcquire。
    }

    // tryAcquire的公平版本。除非是递归调用或没有等待者或者是第一个,否则不授予访问。
    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 ;
    }
}

ReentrantLock.NonfairSync

提供非公平性的锁实现。

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

    // 执行lock。尝试立即闯入,失败就退回常规流程
    final void lock() {
        if (compareAndSetState(0, 1))// 首先进行获取
             // 获取成功,把当前线程设为持有者
            setExclusiveOwnerThread(Thread.currentThread());
        else
             // 获取失败,进入常规流程:acquire会首先调用tryAcquire,
            acquire(1);
    }

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

可以注意到代码实现里的 acquire(1); sync.release(1);,这里都是1,因为加锁与退出的计数步长的绝对值是必须一样的。可以把1换为2,但这样会减少锁可以重入的次数。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据