ReentrantLock

本文详细解释了如何设计一个排他锁,如ReentrantLock,基于AQS框架,涉及数据结构、线程调度、公平性及释放锁机制。重点讲解了lock()和unlock()方法的实现以及AQS中的Node节点和队列管理。

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

之前我们已经学习了Synchronized关键字,我们也知道了Synchronized是一种排他锁,现在如果让你来设计一个排他锁,该怎么实现呢?

// 设计思路
// 1.实现锁的数据结构
- 锁状态(status):记录锁的状态,用于标识锁是否被占用.
- 持有者线程(ownerThread):记录当前持有锁的线程。
- 等待队列(queue):用于存储那些无法获取锁而被阻塞的线程,这些线程在锁释放时会被唤醒。
// 2.线程调度和阻塞机制
- 获取锁机制:实现获取锁的方法,在竞争情况下确保只有一个线程能够成功获取锁。
- 释放锁机制:实现释放锁的方法,在线程退出同步代码块时,释放持有的锁,并唤醒等待队列中的线程
- 线程等待和唤醒:确保当线程无法获取锁时能够进入等待状态,而在锁释放时能够唤醒等待的线程。
- 等待队列管理:管理等待获取锁的线程队列,确保等待的线程按照特定规则获得执行机会

// 大致逻辑
1.当多个线程操作共享资源时,必须先获取lock
2.得到lock的线程才能执行逻辑,没得到lock的线程则进入阻塞状态,并且放入等待队列queue
3.当获取到lock的线程完成后,释放锁,并且唤醒queue中的线程去重新获取锁,得到后则继续执行逻辑

其实上述思路就是我们今天的主角ReentrantLock的底层设计思路.

ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全,比synchronized灵活,比如它支持手动加锁与解锁,支持加锁的公平性。

使用

使用方法很简单,线程操纵资源类就行。主要方法有两个lock()和unlock()

public class ReentrantLockTest {
    private static final ReentrantLock lock = new ReentrantLock(true);
    public static void main(String[] args) {
        // 加锁
        lock.lock();
        try {
            // 访问临界区资源
            // ...
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

底层实现

AQS

ReentrantLock底层是基于AbstractQueuedSynchronizer的,内部有三大属性:head、tail、state

// 队列头部,等待队列中的首节点,它指向第一个正在等待获取锁的线程节点
private transient volatile Node head;
// 队列尾部,等待队列中的尾节点,它指向最后一个正在等待的线程节点
private transient volatile Node tail;
// 同步状态,表示锁的状态,用于记录锁被重入的次数
private volatile int state;

在AQS中, Node对象是用来表示等待队列中的每一个线程节点的,上述的head、tail都为Node对象

// Node对象重要属性
class Node{
  // 节点等待状态
  /*
  CANCELLED (1):表示节点已被取消
  SIGNAL (-1):表示后续节点(当前节点的 next 节点)需要被唤醒。
  CONDITION (-2):表示节点在等待某个条件。
  PROPAGATE (-3):表示 releaseShared 应当传播到其他节点。
  0:表示初始状态。
  */
  volatile int waitStatus;
  // 双向链表当前节点前节点
  volatile Node prev;
  // 下一个节点
  volatile Node next;
  // 线程的引用,表示等待队列中的具体线程
  volatile Thread thread;
  // condition条件等待的下一个节点
  Node nextWaiter;
}

lock() 实现

// 以下列公平锁为例,分析lock()底层逻辑
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
// -> FairSync实现lock()
 final void lock() {
     acquire(1);
}
// -> 进入AQS的acquire(),获取独占锁
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

先直接讲述这几个方法的作用

// tryAcquire(arg) -> 尝试获取锁
// addWaiter(Node.EXCLUSIVE), arg) -> 线程进入等待队列
// acquireQueued() -> 阻塞线程

下面分别讲述方法逻辑

tryAcquire(arg)

尝试获取锁

protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取当前锁的状态
    int c = getState();
    
    // 如果当前锁状态为0(表示当前没有线程持有锁)
    if (c == 0) {
        // 如果当前没有等待线程或者获取锁失败,则尝试CAS设置锁状态为acquires
        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; // 获取锁失败
}
addWaiter()

若获取锁失败,则会加入等待队列

private Node addWaiter(Node mode) {
    // 创建一个新的节点,表示当前线程
    Node node = new Node(Thread.currentThread(), mode);
   
    // 尝试快速添加节点到队列尾部(fast path)
    Node pred = tail; // 获取队尾节点
    if (pred != null) {
        node.prev = pred; // 设置新节点的prev为队尾节点
        // 尝试使用CAS操作将新节点设置为队尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node; // 设置队尾节点的下一个节点为新节点
            return node; // 添加成功,返回新节点
        }
    }
    // 添加节点到队列尾部(full enq)
    enq(node); // 调用enq方法,将节点添加到队列尾部
    return node; // 返回新节点
}

最开始tail节点一定为null,所以会来到enq(node)

private Node enq(final Node node) {
	// 无限循环
    for (;;) {
        Node t = tail; // 获取队尾节点
        if (t == null) { // 如果队尾节点为空,则需要初始化
            // 必须进行初始化,尝试使用CAS操作将一个新的头节点设置为队列的头部
            if (compareAndSetHead(new Node()))
                tail = head; // 初始化尾节点为头节点
        } else {
            node.prev = t; // 设置新节点的prev为队尾节点
            // 尝试使用CAS操作将新节点设置为队尾节点
            if (compareAndSetTail(t, node)) {
                t.next = node; // 设置队尾节点的下一个节点为新节点
                return t; // 返回原始的队尾节点
            }
        }
    }
}

第一次循环:刚开始等待队列是空的,所以会先初始化,初始化出来的队列如下图:head、tail都指向新创建Node节点,waitStatus = 0,thread = null
在这里插入图片描述

第二次循环:已经创建好了等待队列,现在需要把代表当前线程的节点入队,如下图:
在这里插入图片描述

acquireQueued()

将当前线程构建完Ndoe并放入等待队列后,下面则是去阻塞线程.

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);
                // 清空前驱节点的 next 引用,帮助 GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果应该在获取失败后阻塞线程,并且阻塞期间被中断过
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 这里会进行阻塞线程
                interrupted = true; // 设置中断标志
        }
    } finally {
        // 如果获取锁失败,取消获取(将节点从等待队列中移除)
        if (failed)
            cancelAcquire(node);
    }
}

// 说明
1.第一次进来的时候,会重新尝试获取锁,因为在刚才入队的过程中,持有锁的线程可能已经释放了锁.如果前驱节点是头节点并且尝试获取锁成功,则将当前节点设置为头节点,表示获取锁成功。
2.获取锁失败,则会执行shouldParkAfterFailedAcquire(p, node),这里第一次会将前驱节点的waitStatus设置为-1(SIGNAL),因为持有锁的线程在释放锁的时候,会判断head节点的waitestate是否!=0,如果!=0成立会再把waitstate = -1->0.
3.第一次循环shouldParkAfterFailedAcquire(p, node)返回false,第二次循环才会去阻塞线程parkAndCheckInterrupt().

到这里为止,lock()方法就阻塞了线程,接下来我们来看下唤醒unlock的逻辑.

unlock() 实现

tryRelease(arg) 释放锁
unparkSuccessor(h); 唤醒节点

lock.unlock();
 // ->
sync.release(1);
 // ->
 public final boolean release(int arg) {
    // 尝试释放锁
    if (tryRelease(arg)) {
        // 获取头节点
        Node h = head;
        // 如果头节点不为空,并且头节点的状态不为0(对应shouldParkAfterFailedAcquire(p, node))
        if (h != null && h.waitStatus != 0)
            // 唤醒后继节点
            unparkSuccessor(h);
        return true; // 释放锁成功
    }
    return false; // 释放锁失败
}
tryRelease() 释放锁
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; // 标记锁已经被完全释放
        setExclusiveOwnerThread(null); // 将持有锁的线程置为null
    }
    setState(c); // 更新锁的状态值
    return free; // 返回锁是否完全释放的标志
}

unparkSuccessor(h) 唤醒节点
// 传入的node为head头节点
private void unparkSuccessor(Node node) {

     // 这里又把head的waitStatus从-1改为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 获取头节点的下一个节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从尾部向前搜索,直到找到一个等待状态不大于0的节点(未被取消的后继节点)
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果找到了合适的后继节点,则唤醒其对应的线程
    if (s != null)
        LockSupport.unpark(s.thread); // 唤醒线程
}

// 这里执行后,之前阻塞线程就会唤醒
LockSupport.unpark(s.thread); 
// 这是之前阻塞的代码块
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()) // 1.从这里继续执行,继续走循环
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

总结

下图方便理解
在这里插入图片描述

这里也只是针对简单的情况进行描述,将大致的流程描述出来,过程晕晕的,AQS将太多场景柔和在了一个框架中,只能说大牛真牛。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值