ReentrantLock 的源码实现

今天是第一篇,我想聊下ReentrantLock,在早期的时候,可能大家都在好奇是怎么实现的。今天我带着大家来看一下。话不多说,发车。

我们就从最简单的lock方法开始

csharp 复制代码
public ReentrantLock() {
    sync = new NonfairSync();
}

众所周知,ReentrantLock是基于aqs实现的。这里lock就是创建了一个非公平锁的序列。 大家需要记住,aqs有2个比较重要的东西

arduino 复制代码
private volatile int state;
arduino 复制代码
volatile int waitStatus;
  1. state是aqs同步队列的状态,0代表没有线程持有锁,大于0代表有线程持有锁,只有等于0的时候,其他线程才有可能拿到锁。
  2. waitStatus 就是每个node节点的状态,这里就要引出来为什么很多八股说aqs是同步队列了,当锁竞争的时候,没有拿到锁的线程就被封装成一个Node节点,等待获取锁。

现在我们详细看下aqs的NonfairSync的lock方法

scala 复制代码
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

首先,cas 设置获取state=1,设置当前线程独占(获取锁),如果失败,走acquire方法

scss 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这个acquire方法做了2件事情

  1. 再一次尝试获取锁
  2. 加入队列

tryAcquire我们具体看下

arduino 复制代码
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
ini 复制代码
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;
}
  1. 获取state是否等于0,等于0就可以尝试获取锁,设置独占,返回true
  2. 如果不等于0,判断是不是当前线程占有的,如果是的话,就把state加1,朋友们,这里就是重入的意思,这里也解释了aqs为什么每次lock和unlock是一对的,不然其他线程是获取不到锁的

没有获取到锁的情况下,我们尝试加入队列,现在看下加入队列的方法

ini 复制代码
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
  1. 把当前线程封装成一个node节点。获取aqs尾部为pred
  2. 如果tail==null,意味着需要初始化,enq方法进行初始化,把当前节点加到队列尾部
  3. 不为null,把当前节点加到队列尾部
ini 复制代码
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  1. 获取tail节点,如果tail节点等于null,设置一个空节点为head节点,空节点也为tail节点,就是头尾都指向这个空节点
  2. 否则就把当前节点放到tail节点之后

大家注意下这个方法,这是一个for循环,初始化完之后,再一次执行,把当前节点放到这个空节点的后面。

加入队列以后,我们再来看acquireQueued方法

ini 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
java 复制代码
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}
ini 复制代码
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  1. 获取当前节点的前一个节点,赋值为p
  2. 如果p是head节点,并且当前线程获取锁成功,设置当前节点为头节点,这里需要画一个图表示,毫无疑问哈,如果上一个线程没有释放锁,自然不可能获取锁成功,继续往下走,看shouldParkAfterFailedAcquire方法
  3. shouldParkAfterFailedAcquire 获取前一个节点,如果是-1,直接返回true,如果前一个节点的的waitStaus 大于0,意味着对方不排队了,所以需要断开连接,等于从队列里面踢出去,保留小于0的
  4. 设置前一个节点的waitStaus 为-1 也就意味着下一次循环的时候,就会返回false

如下图所示

初始状态

shouldParkAfterFailedAcquire第一次

scss 复制代码
if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())

此刻shouldParkAfterFailedAcquire就为true了,因为前一个节点waitStatus就为-1了,我们再看下parkAndCheckInterrupt方法

arduino 复制代码
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

非常简单挂起当前线程,lock的流程就结束了lock所有的

再来看下unlock方法

csharp 复制代码
public void unlock() {
    sync.release(1);
}
java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
ini 复制代码
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;
}
ini 复制代码
private void unparkSuccessor(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)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}
  1. 获取state==0,设置独占锁为null,设置状态为0,retrun true
  2. 如果释放锁成功(没有任何线程占有锁),获取头节点,并且头节点的waitstatus!=0,也就是意味着有线程排队。设置头结点的下一个线程的waitStatus为0
  3. 如果head的下一个节点不存在或者退出了,从尾部开始遍历,找到距离头节点最近的等待的节点,然后执行unPark方法,唤醒这个线程

唤醒之后就会再进acquireQueued方法,如果抢到锁,当前节点就为头节点,跟头节点断开连接,如下图所示

ini 复制代码
final boolean acquireQueued方法(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

总结,加入队列的初始node,waitStatus是0,会把前一个节点的waitStaus的状态变为-1,释放锁的时候,会把队列第一个节点的waitStatus变成0

相关推荐
罗政2 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师3 小时前
spring获取当前request
java·后端·spring
Java小白笔记5 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
JOJO___7 小时前
Spring IoC 配置类 总结
java·后端·spring·java-ee
白总Server8 小时前
MySQL在大数据场景应用
大数据·开发语言·数据库·后端·mysql·golang·php
Lingbug9 小时前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore
计算机学姐9 小时前
基于SpringBoot+Vue的篮球馆会员信息管理系统
java·vue.js·spring boot·后端·mysql·spring·mybatis
好兄弟给我起把狙9 小时前
[Golang] Select
开发语言·后端·golang
程序员大金9 小时前
基于SpringBoot+Vue+MySQL的智能物流管理系统
java·javascript·vue.js·spring boot·后端·mysql·mybatis
ac-er888810 小时前
在Flask中处理后台任务
后端·python·flask