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

相关推荐
喝养乐多长不高1 小时前
Spring Web MVC基础理论和使用
java·前端·后端·spring·mvc·springmvc
莫轻言舞1 小时前
SpringBoot整合PDF导出功能
spring boot·后端·pdf
玄武后端技术栈2 小时前
什么是死信队列?死信队列是如何导致的?
后端·rabbitmq·死信队列
老兵发新帖4 小时前
NestJS 框架深度解析
后端·node.js
码出钞能力5 小时前
对golang中CSP的理解
开发语言·后端·golang
金融数据出海5 小时前
黄金、碳排放期货市场API接口文档
java·开发语言·spring boot·后端·金融·区块链
豌豆花下猫5 小时前
Python 潮流周刊#101:Rust 开发的 Python 类型检查工具(摘要)
后端·python·ai
gxn_mmf6 小时前
典籍知识问答模块AI问答功能feedbackBug修改+添加对话名称修改功能
前端·后端·bug
向哆哆7 小时前
Spring Boot快速开发:从零开始搭建一个企业级应用
java·spring boot·后端
[email protected]9 小时前
ASP.NET Core 中实现 Markdown 渲染中间件
后端·中间件·asp.net·.netcore