ReentrantLock 原理源码解读

本文详细剖析了ReentrantLock的工作原理,包括非公平锁的实现、锁获取与释放流程、线程阻塞与唤醒机制等核心内容,并通过具体示例帮助理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 上图是我引用的图片。绿色的虚线代表实现接口,红色线代表内部类(比如Sync是ReentrantLock的内部类),蓝色线代表继承。

先从构造器开始看,默认为非公平锁实现

public ReentrantLock() {
    sync = new NonfairSync();
}

NonfairSync 继承自 AQS

reentrantLock.lock()流程

public void lock() {
    sync.lock();// 这里看sync的运行时类,先考虑非公平锁,即NonfairSync
}
final void lock() {
    if (compareAndSetState(0, 1))// AQS中,用cas方式修改state从0->1,0代表为加锁,1代表加锁1次
        setExclusiveOwnerThread(Thread.currentThread());// 设置当前线程为锁的持有者
    else// 如果cas失败,说明有线程曾经上过锁,注意:之前上锁的那个线程也有可能是当前线程(重入)
        acquire(1);// AQS中的方法
}
// 能进入这个方法,说明当前的state已经不是0了
public final void acquire(int arg) {// arg = 1
    if (!tryAcquire(arg) &&// 尝试获得锁,如果成功,!tryAcquire(arg) = false,短路与直接退出
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 尝试获取锁失败,执行acquireQueued
        selfInterrupt();// 自我打断
}

tryAcquire(arg)逻辑,这个方法是AQS中定义的,希望子类去重写的方法,下面是AQS中的tryAcquire实现

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

显然AQS是不希望你调用到AQS中的tryAcquire的。

查找子类实现,由于此时this的运行时类是NonfairSync(非公平锁),所以去NonfairSync中去找tryAcquire

 

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) { // acquire = 1
    final Thread current = Thread.currentThread();// 当前线程
    int c = getState();// 获取状态,这里已经不是0了
    if (c == 0) {// 如果其他线程已经释放锁了,会把state变成0,释放锁的逻辑后面说,这里假设没有释放锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {// 如果当前线程是锁的持有者,锁重入了
        int nextc = c + acquires;// 下一个state += 1
        if (nextc < 0) // 如果当前state = (2^31) - 1那么 state + 1 = -(2^31),证明整数越界
            throw new Error("Maximum lock count exceeded");// 这里肯定不会
        setState(nextc);// 更新后的值就是2了,证明当前线程加了2次锁,锁重入计数为2
        return true;
    }
    return false;// 如果当前state不为0(未释放锁),并且当前线程也不是锁目前的持有者,尝试获得锁失败
}

所以尝试获得锁成功,要么是因为锁已经被其他线程释放了(state重新改成0),要么就是当前线程就是锁的持有者(锁重入)

尝试获得锁失败,是因为锁的持有者不是当前线程,并且持有锁的那个线程还没有释放锁

if (!tryAcquire(arg) &&// 这里如果获得锁成功,就直接返回了
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 获取锁失败,这个方法中会让线程阻塞
    selfInterrupt();
}

我们先看一下addWaiter(Node.EXCLUSIVE)的逻辑,Node.EXCLUSIVE就是Node定义的一个常量表示独占锁

private Node addWaiter(Node mode) {// 节点的模式,AQS$Node规定了两种模式,独占锁和共享锁
    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) {// 第一次tail就是空
        node.prev = pred;// 如果不是第一次进入addWaiter,先把当前节点的钱去接待你指向tail
        if (compareAndSetTail(pred, node)) {// 把tail用cas方式设置成新创建的node
            pred.next = node;// 将以前尾节点的next域指向当前新创建的节点
            return node;// 返回新创建的节点
        }
    }
    enq(node);// ⬇看下面,第一次tail是空,就会走这个方法
    return node;// 返回新创建的节点
}

private Node enq(final Node node) {// 这个节点就是新创建的那个节点
    for (;;) {// 死循环
        Node t = tail;// 当前tail,仍然是空
        if (t == null) { // Must initialize 必须初始化
            if (compareAndSetHead(new Node()))// cas方式设置头节点,头节点不保存任何线程和模式
                tail = head;// 让tail指向头节点,此时head和tail都指向同一个节点
        } else {// 多个线程同时调用enq时,casHead操作只会有一个线程成功,对于cas失败的线程就会进入下一次循环,下一次循环就会走else,因为tail已经不是空了。cas成功的线程也要进入下一次循环把自己的node挂到tail后面
            node.prev = t;// 当前节点的前驱节点指向tail
            if (compareAndSetTail(t, node)) {// cas修改tail的指向,如果失败进入下一次循环
                t.next = node;// 之前的tail的next域指向当前节点
                return t;// 返回之前的tail节点
            }
        }
    }
}

addWaiter内部会创建一个保存当前线程的node,并把node挂载到之前的tail后面,让tail重新指向node(代表node是尾节点),最后方法的返回值就是新创建的这个node。会产生一个双向链表,返回Node(Thread-1)

 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)逻辑

// 参照上图,node就是Node(Thread-1),arg = 1
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;// 失败标志,默认失败了,看finally块(暂时不重要)
    try {
        boolean interrupted = false;// 当前是否被打断标志,默认没有
        for (;;) {// 死循环
            final Node p = node.predecessor();// 返回上一个节点,就是图中的Node(null)
            if (p == head && tryAcquire(arg)) {// 如果p是头节点(这里确实是),再次尝试获得锁(刚刚说过,去找NonfairSync中对应的方法),如果获得锁成功
                setHead(node);// 获得锁成功,Node(Thread-1)没必要保存了,因为Thread-1不需要阻塞,看下面setHead的源码,就是要将Node(Thread-1)设置成头节点
                p.next = null; // help GC 前驱节点也不指向node了,那么这个前驱节点就会被垃圾回收
                failed = false;// 没有失败,方式finally中取消获得锁的请求
                return interrupted;// 返回中端标记
            }
            // 如果node的前驱节点p不是头节点,或者尝试获得锁又失败了
            if (shouldParkAfterFailedAcquire(p, node) &&// 尝试获得锁失败后应该被暂停吗,看下面,可知第一次循环返回false,就会直接进入下一次循环,如果又发生(node的前驱节点p不是头节点,或者尝试获得锁又失败了),第二次调用shouldParkAfterFailedAcquire返回true
                parkAndCheckInterrupt())// 进入真正暂停线程的方法,直到当前线程被打断或者unpark后继续运行
                interrupted = true;// 如果parkAndCheckInterrupt返回true,把打断标志记录下来
            // 注意,即使线程被打断了,仍然会进入下一次循环尝试获得锁,这也是为什么lock.lock()后调用interrupt不会立即中断的原因
        }
    } finally {
        if (failed)// 如果失败了
            cancelAcquire(node);// 就取消当前获得锁的请求
    }
}

private void setHead(Node node) {
    head = node;// 设置为头节点
    node.thread = null;// 把Node(Thread-1)中的线程去掉
    node.prev = null;// 不再指向前驱(Node(null))节点
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;// 前驱节点的状态,最开始进来是0,因为节点的状态还没有修改过
    /*
    在Node类中规定了4种状态:
    	static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
    */
    if (ws == Node.SIGNAL)// 如果是-1,第二次调用就是-1,因为第一次调用的时候已经改成-1了
        return true;// 返回应该被暂停
    if (ws > 0) {// 如果是1,代表当前线程被取消了
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {// 第一次走这里
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将前驱节点的状态从0->-1
    }
    return false;// 第一次返回false
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);// 如果当前线程的打断标志为false,暂停当前线程,如果是true则不会阻塞
    // 这里当前线程就阻塞住了,从WAITING->RUNNABLE的条件,要么是其他线程调用了当前线程的interrupt方法,要么是调用了LockSupport.unpark(当前线程对象)
    return Thread.interrupted();// 返回当前线程是否被打断,如果打断了,同时清除打断标志
}

由此,线程1会进入阻塞状态,那它什么时候会被唤醒呢?(先不考虑interrupt)

对了,就是LockSupport.unpark(Thread-1线程对象),该操作其实会在释放锁流程中执行

锁释放原理

public void unlock() {// reentrantLock里面的unlock
    sync.release(1);
}
public final boolean release(int arg) {// AQS中的释放流程
    if (tryRelease(arg)) {// 同样tryRelease也是子类需要实现的,找NonfairSync或者Sync
        // 如果锁释放成功
        Node h = head;
        if (h != null && h.waitStatus != 0)// 头节点不是空,且头节点的状态不是0,之所以要加!=null的校验,是因为如果h是空,证明没有发生锁竞争,就是当前线程在获得锁到释放锁期间没有其他线程调用acquire,自然就没必要去唤醒处于阻塞的线程(没有嘛)。h.waitStatus != 0为什么加后面讲
            unparkSuccessor(h);// 重新唤醒阻塞中的线程
        return true;// 释放锁成功
    }
    return false;// 释放锁失败
}

看Sync类(NonfairSync的直接父类)中的tryRelease(int)

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;// state - 1,表示锁重入计数-1,因为一个线程获取n次锁,state=n
    if (Thread.currentThread() != getExclusiveOwnerThread())// 如果当前锁是A线程持有的,结果B线程来释放锁,必然不合理,抛出异常
        throw new IllegalMonitorStateException();
    boolean free = false;// 当前锁是否空闲
    if (c == 0) {// 如果锁重入计数=0,就代表当前线程可以将真正锁了
        free = true;// 锁释放成功了
        setExclusiveOwnerThread(null);// owner会在下一次竞争重新被赋值
    }
    setState(c);
    return free;
}

可见tryRelease中,判断是否释放锁成功,就是看锁重入计数(state)是不是为0了,为0就成功释放,不为0,证明当前线程加了n次锁,只释放了不到n次,那这把锁还是应该属于当前线程的,所以锁释放失败。

下面考虑,锁真正释放后,会执行的unparkSuccessor(h)

private void unparkSuccessor(Node node) {// node这里的实参就是头节点
    int ws = node.waitStatus;// 头节点的状态,不知道大家还记不记得在Thread-1 park前,曾经获得过它的前驱节点,并在shouldParkAfterFailedAcquire方法中将其值设为了-1
    if (ws < 0)// 这里确实<0
        compareAndSetWaitStatus(node, ws, 0);// 将头节点状态改成0,代表我要开始唤醒处于阻塞的线程了,现在解释一下为什么要在release中加h.waitStatus != 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)// 如果头节点的下一个节点不是null,
        LockSupport.unpark(s.thread);// 唤醒Thread-1
}

线程1被唤醒后,应该执行

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);// 正常unpark
    return Thread.interrupted();// false
}
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)) {// 这里又去尝试获得锁,但是这里还是可能会失败,确实unparkSuccessor方法只唤醒了一个线程(就是当前这个线程),挂载在Node(Thread-1)后面的线程都不会被唤醒,但是可能会有外部线程调用lock.lock(),但大概率是会成功的
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())// 返回false,进入下一次循环
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

思考如下问题

Lock lock = new ReentrantLock();// 创建的锁默认是公平还是非公的?
new Thread(() -> {
    lock.lock();
    Thread.sleep(1000000);// catch异常省略
}, "t1").start();
Thread.sleep(1000);
lock.lock();// 解释这里为什么会阻塞住,底层用的是什么方式?
System.out.println("成功");
Lock lock = new ReentrantLock();
new Thread(() -> {
    lock.lock();
    Thread.sleep(10000);// catch异常省略
    // 释放锁
}, "t1").start();
Thread t2 = new Thread(() -> {
    Thread.sleep(1000);
    lock.lock();// 这里会阻塞
    System.out.println("t2后续代码");
    // 请问,如果t1在10秒后释放锁后,下面这个语句会不会输出,如果会输出,输出什么,为什么?
    System.out.println(Thread.currrentThread().isInterrupted());
}, "t2");
t2.start();
Thread.sleep(2000);// 主线线程2秒后
t2.interrupt();// 请问这里打断t2,会不会立即输出"t2后续代码",为什么?
System.out.println("成功");
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值