谈谈可重入锁ReentrantLock

811 阅读4分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

ReentrantLock简介

ReentrantLock是Java并发包中提供的一个可重入的互斥锁。实现于Lock接口,作用与synchronized相同,不过对比于synchronized更加灵活,但是使用时需要我们手动获取/释放锁。ReentrantLock和synchronized在基本用法,行为语义上是类似的,都具有可重入性。

ReentrantLock实现

ReentrantLock的所有锁相关操作都是通过Sync类实现,Sync继承于AbstractQueuedSynchronizer同步队列,并实现一些通用的接口实现。NonfairSync继承于Sync,实现了非公平的方式获取锁;FairSync继承于Sync,实现了公平的方式获取锁。 可重入.PNG

ReentrantLock所提供的一些方法如下:

// 查询当前线程调用lock()的次数
int getHoldCount() 

// 返回目前持有此锁的线程,如果此锁不被任何线程持有,返回null  
protected Thread getOwner(); 

// 返回一个集合,它包含可能正等待获取此锁的线程,其内部维持一个队列(后续分析)
protected Collection<Thread> getQueuedThreads(); 

// 返回正等待获取此锁资源的线程估计数
int getQueueLength();

// 返回一个集合,它包含可能正在等待与此锁相关的Condition条件的线程(估计值)
protected Collection<Thread> getWaitingThreads(Condition condition); 

// 返回调用当前锁资源Condition对象await方法后未执行signal()方法的线程估计数
int getWaitQueueLength(Condition condition);

// 查询指定的线程是否正在等待获取当前锁资源
boolean hasQueuedThread(Thread thread); 

// 查询是否有线程正在等待获取当前锁资源
boolean hasQueuedThreads();

// 查询是否有线程正在等待与此锁相关的Condition条件
boolean hasWaiters(Condition condition); 

// 返回当前锁类型,如果是公平锁返回true,反之则返回flase
boolean isFair() 

// 查询当前线程是持有当前锁资源
boolean isHeldByCurrentThread() 

// 查询当前锁资源是否被线程持有
boolean isLocked()

非公平锁源码中的加锁流程如下:

//非公平锁NonfairSync的实现,其继承于Sync
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    //lock接口实现;
    //首先通过CAS试图获取锁,获取成功则设置锁的Owner;
    //否则调用acquire获取锁,acquire又或调用tryAcquire获取锁,
    //而tryAcquire是通过非公平的方式获取锁。
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    //非公平方式获取锁
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

这块代码的含义为:

若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。

若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入Acquire方法进行后续处理。

再看下公平锁源码中获锁的方式:

//公平锁FairSync 的实现,其继承于Sync
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    //lock接口实现,自己调用acquire获取锁;
    //acquire又会调用tryAcquire获取锁,而tryAcquire是通过公平(FIFO)
    //的方式获取锁。
    final void lock() {
        acquire(1);
    }

    //公平的方式获取锁
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        //获取当前锁状态,此状态c>0表示有线程获取到锁,重入的次数为c
        int c = getState();
        //无线程获取锁?
        if (c == 0) {
            //当前节点无前驱节点并且当前线程CAS更新状态成功;、
            //表示当前线程公平的获取到锁
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //获得锁的线程就是当前线程?则获取次数加1,并设置状态(即次数)
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

通过源码分析可知,当某个节点获取到锁时,会通过setExclusiveOwnerThread()方法记录获取独占锁的线程Thread;当某个线程获取锁时,当锁已被占用,会判断占用锁的线程是否为当前线程;是则直接更新锁状态,表示获取到锁;否则获取锁失败。

公平锁是通过FairSync实现的,其在tryAcquire获取锁时,会判断同步队列中当前节点是否有前驱节点;有前驱节点,则获取锁失败,进入同步队列,等待获取锁;无前驱节点时,表示当前节点是同步队列中等待锁时间最长的节点,则当前节点优先获取锁资源。

非公平锁是通过NonfairSync实现的,其在lock及tryAcquire时,会先通过CAS的方式尝试获取锁,获取失败才会进入同步队列等待。这就导致当某个线程刚释放锁,而同步队列中被unpark的头节点还未CAS获取到锁的时间间隙,当前线程先于同步队列头结点通过CAS获取锁。使得某些线程会等待很长时间才会获得锁,这是非公平性的。

ReetrantLock中的unlock()释放锁

在使用ReetrantLock这类显式锁时,获取锁之后也需要手动释放锁资源。unlock()释放锁的代码如下:

// ReetrantLock → unlock()方法
public void unlock() {
    sync.release(1);
}

// AQS → release()方法
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 → Sync → tryRelease(int releases)方法
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;
      // 设置Owner为null
      setExclusiveOwnerThread(null);
  }
  // 设置更新同步状态
  setState(c);
  return free;
}

unlock()方法调用tryRelease(int releases)释放锁的,而tryRelease(int releases)则是ReetrantLock实现的方法,因为在AQS中没有提供具体实现,释放锁资源后会使用unparkSuccessor(h)唤醒后继节点的线程。unparkSuccessor(h)的代码如下:

private void unparkSuccessor(Node node) {
    // node一般为当前线程所在的节点,获取当前线程的等待状态
    int ws = node.waitStatus;
    if (ws < 0) // 置零当前线程所在的节点状态,允许失败
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next; // 获取当前节点的后继节点
    if (s == null || s.waitStatus > 0) { // 如果为空或已结束
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            // 等待状态<=0的节点,代表是还有效的节点
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread); // 唤醒后继节点线程
}

总结

ReetrantLock总结。同步状态标识:对外显示锁资源的占有状态。同步队列:存放获取锁失败的线程。等待队列:用于实现多条件唤醒。Node节点:队列的每个节点,线程封装体。cas修改同步状态标识,获取锁失败加入同步队列阻塞,释放锁时唤醒同步队列第一个节点线程。

加锁过程:调用tryAcquire()修改标识state,成功返回true执行,失败加入队列等待。加入队列后判断节点是否为signal状态,是就直接阻塞挂起当前线程。如果不是则判断是否为cancel状态,是则往前遍历删除队列中所有cancel状态节点。如果节点为0或者propagate状态则将其修改为signal状态。阻塞被唤醒后如果为head则获取锁,成功返回true,失败则继续阻塞。

解锁过程:调用tryRelease()释放锁修改标识state,成功则返回true,失败返回false。释放锁成功后唤醒同步队列后继阻塞的线程节点,被唤醒的节点会自动替换当前节点成为head节点。