并发学习(五)从ReentrantLock走进AQS

本文详细解析了ReentrantLock与AQS的工作原理,包括非公平锁和公平锁的实现过程,以及它们之间的主要区别。通过生动的故事场景,阐述了线程如何竞争和获取锁,以及锁的释放机制。

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

通过前面几章的学习,我的脑海里回顾最多的一句话就是:多个线程争抢一把锁,抢到锁的线程执行下面的逻辑,没抢到锁的线程则阻塞。
下面来看一段ReentrantLock的简单代码:

private Lock lock = new ReentrantLock();
private void sayHello() {
    lock.lock();
    System.out.println("hello");
    lock.unlock();
}

这段代码只需要lock和unlock一下就能保证多线程环境下数据的安全性,其实它的内部实现就是通过AQS。

从ReentrantLock走进AQS

我不会把里面的源码都贴出来看,而是尽可能的通过故事情节来描述,也就是所谓的从业务角度来分析。(情节采用的是默认的非公平锁)

情节1: 从前有三个线程:线程A、线程B和线程C。他们都访问了sayHello方法,并且执行了代码:lock.lock();

情节2: compareAndSetState(0, 1)就是CAS操作,类似数据库的乐观锁。多个线程同时去CAS,但是只会有一个成功。举例:AQS里有个共享变量state=0(可以理解为锁标记:0表示无锁,1表示第一次获得锁,大于1表示多次获得锁,也就是重入锁次数),线程A首先会去看state是否等于0,如果等于则更新为1。此时线程B再去CAS时,发现state=1,说明已经有其他线程进来了,所以线程B CAS失败。

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

假设,线程A CAS成功了,那么就会将线程所有者设置为线程A,宣誓线程A获得锁,然后线程A继续执行System.out.println(“hello”);
在这里插入图片描述

情节3: 线程A获得锁后,就开始执行后续代码了,线程B和线程C就只能走acquire(1);

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

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;
 }

线程B/C的侥幸心理认为:万一线程A获得锁很快就释放了呢?所以我应该再去尝试一次,于是乎读取int c = getState();如果c=0表示线程A确实释放锁了,那么线程B/C再去CAS尝试获得锁。

情节4: 然而,线程B/C的侥幸心理并没有实现,此时的线程A还是没有释放锁。因此,线程B/C都进入自旋状态。

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;
}

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;
            }
        }
    }
}

执行Node node = new Node(Thread.currentThread(), mode);线程B/C都被会封装成Node节点。而执行到compareAndSetHead(new Node())时,这里又有一个CAS操作,就看谁先成功,谁就先创建一个空的Node节点。
在这里插入图片描述

情节5: 假设线程B CAS成功了,线程B会创建一个空的Node结点,并且线程节点B和空节点Node会形成双向链表。
在这里插入图片描述
情节6: 类似的,线程节点C会跟在线程节点B的后面,并且tail指针指向线程节点C。
在这里插入图片描述
情节7: 此时,线程B和线程C都已经加入同步队列了,但是不可能一直就在里面自旋着。他们有2个选择:第一,尝试再去获取锁。第二,如果还是获取锁失败,那么就只能阻塞了。

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);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

这里会将head节点以及它的next节点waitStatus设置为SIGNAL,并且调用LockSupport.park(this);将队列里的线程挂起。这时候,线程B和线程C趋于稳定,后续就等待线程A释放锁了。
在这里插入图片描述

情节8: 线程A执行完System.out.println(“hello”);并且调用了lock.unlock();释放锁。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

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;
}

线程A释放锁实际上也分为两步:第一,设置exclusiveOwnerThread = null以及state=0。第二,唤醒一个线程。这里会直接找到头结点,然后唤醒它的next结点,也就是唤醒线程B。线程B被唤醒后,重新去CAS,由于此时state=0,所以CAS成功,线程B最终获得了锁。至于为啥唤醒的是线程B而不是线程C,原因就在于之前线程B优先CAS成功,并创建了空的Node结点,使得它可以成为头结点的next结点。所以,这个链表也是FIFO的双向链表。
在这里插入图片描述

情节9: 线程B被唤醒后,就会沿着它原来阻塞的位置继续执行,链表里的线程节点B也就失去了意义。这里会将线程节点B的thread设置为null,并且head指针指向节点B。原来的头结点被删除了,节点B成为了新的头结点。
在这里插入图片描述
以上就是ReentrantLock的非公平锁获得锁和释放锁的过程。

公平锁和非公平锁的区别

以上的原理分析针对的是非公平锁,它还有公平锁的实现。公平锁的AQS逻辑基本跟非公平锁一致,下面来看下两者的区别。
公平锁: 严格按照执行顺序来,也就是等待时间越长的线程越先获得锁。
非公平锁: 允许插队,线程获得锁的几率是个随机事件。

lock方法差异
公平锁:调用acquire(1);

final void lock() {
    acquire(1);
}

非公平锁:一上来就CAS,谁先CAS成功谁获得锁,完全是个概率时间。

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

tryAcquire方法差异
公平锁:多了!hasQueuedPredecessors(),其他跟公平锁完全一致。解释:如果当前队列已经有其他线程阻塞了,就不让当前线程CAS。更通俗的说:能执行到(!hasQueuedPredecessors()说明当前没有任何一个线程获得锁,此时新来的线程想要CAS获得锁,那不明摆着插队么?因此加这个判断,就是会先让已经存在队列里的线程优先去CAS获得锁,这样才能体现公平性。

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        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;
}

非公平锁:不管是新来的线程还是队列里的线程,都有机会CAS获得锁。

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;
}

补充非公平插队

假设线程A释放锁了,然后头结点的next节点线程B被唤醒了,他重新去CAS,结果被突然而来的线程D先CAS成功了。很苦逼,尽管线程B被唤醒,但是由于再次争抢锁失败,所以还是只能继续待着同步队列里。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值