关于java:不看源码就硬聊AQS实现原理

58次阅读

共计 2754 个字符,预计需要花费 7 分钟才能阅读完成。

看了下 AQS 的源码,有点简单,不适宜简略入门,我总结了下。

概述

Java 中的大部分同步类(Lock、Semaphore、ReentrantLock 等)都是基于 AbstractQueuedSynchronizer(简称为 AQS)实现的。AQS 是一种提供了原子式治理同步状态、阻塞和唤醒线程性能以及队列模型的简略框架。

从 ReentrantLock 独占模式看 AQS 原理

public void test () throw Exception {
    // 初始化
    ReentrantLock lock = new ReentrantLock(true);
    // 加锁
    lock.lock();
    try {...} finally {lock.unlock();
    }
}

lock()


看看非偏心锁的实现:

外围流程从这里开始:

1.compareAndSetState():线程进来间接利用 CAS 尝试抢占锁;setExclusiveOwnerThread():如果抢占胜利 state 值会被改为 1,且设置对象独占锁线程为以后线程

2.acquire(1):若利用 CAS 尝试抢占锁失败,也就是获取锁失败,则进入 Acquire 办法进行后续解决
Acquire 办法实现:

1.tryAcquire():再次尝试获取锁,如果加锁胜利则返回 true, 不再执行以下步骤,否则继续执行以下步骤
2.addWaiter():走到这里阐明加锁失败,创立一个 Node 节点绑定以后的线程,退出到一个 FIFO 的双向链表中,而后返回这个 Node
3.acquireQueued():这个办法会先判断以后传入的Node 对应的前置节点是否为 head 节点,如果是则尝试加锁,如果加锁失败或者 Node 的前置节点不是 head 节点,用 LockSupport.park() 挂起以后线程。
上述流程图:

unlock()



1.tryRelease():state被设置成 0,Lock 对象的独占锁被设置为 null
2.unparkSuccessor():唤醒 head 的后置节点,被唤醒的线程二会接着尝试获取锁,用 CAS 指令批改 state 数据。
上述流程图:

总结:
AQS中 保护了一个 volatile int state(代表共享资源)和一个FIFO 线程期待队列(多线程争用资源被阻塞时会进入此队列)。
这里 volatile 可能保障多线程下的可见性,当 state=1 则代表以后对象锁曾经被占有,其余线程来加锁时则会失败,加锁失败的线程会被放入一个 FIFO 的期待队列中,比列会被 UNSAFE.park() 操作挂起,期待其余获取锁的线程开释锁才可能被唤醒。
另外 state 的操作都是通过 CAS 来保障其并发批改的安全性。

非偏心锁和偏心锁

线程二 开释锁的时候,唤醒被挂起的 线程三 线程三 执行 tryAcquire() 办法应用 CAS 操作来尝试批改 state 值,如果此时又来了一个 线程四 也来执行加锁操作,同样会执行 tryAcquire() 办法。这种状况就会呈现竞争,线程四 如果获取锁胜利,线程三 依然须要待在期待队列中被挂起。这就是所谓的 非偏心锁 线程三 辛辛苦苦排队等到本人获取锁,却眼巴巴的看到 线程四 插队获取到了锁。
非偏心锁 执行流程:

偏心锁在加锁的时候,会先判断 AQS 期待队列中是存在节点,如果存在其余期待线程,那么本人也会退出到期待队列尾部,做到真正的先来后到,有序加锁。
偏心锁 执行流程:

非偏心锁和偏心锁的区别:
非偏心锁性能高于偏心锁性能。非偏心锁能够缩小 CPU 唤醒线程的开销,整体的吞吐效率会高点,CPU也不用取唤醒所有线程,会缩小唤起线程的数量
非偏心锁性能尽管优于偏心锁,然而会存在导致线程饥饿的状况。在最坏的状况下,可能存在某个线程始终获取不到锁。不过相比性能而言,饥饿问题能够临时疏忽,这可能就是 ReentrantLock 默认创立非偏心锁的起因之一了。

从 CountDownLatch 共享模式看 AQS 原理

    void test() throws Exception {CountDownLatch latch = new CountDownLatch(2);
        new Thread(new Runnable() {
            @Override
            public void run() {System.out.println("线程 1 执行");
                latch.countDown();}
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {System.out.println("线程 2 执行");
                latch.countDown();}
        }).start();
        latch.await();
        System.out.println("线程 3 执行");
    }

初始化


初始化 state 值,当 state 值 >0 代表锁被占有,= 0 阐明锁被开释

await()




1.tryAcquireShared():state 不等于 0 的时候,tryAcquireShared()返回的是 -1,此时获取锁失败,也就是说 count 未减到 0 的时候所有调用 await()办法的线程都要排队。
2.doAcquireSharedInterruptibly():创立一个 Node 节点绑定以后的线程,退出到一个 FIFO 的双向链表中,先判断以后传入的 Node 对应的前置节点是否为 head 节点,如果是则尝试加锁,如果加锁失败或者 Node 的前置节点不是 head 节点,用 LockSupport.park() 挂起以后线程。

countDown()



1.tryReleaseShared():开释锁,通过自旋的 CAS 操作对 state-1,如果 state=0, 返回 true 执行 doReleaseShared()
2.doReleaseShared():唤醒期待 await()的线程

总结

独占模式流程:
1.tryRequire()办法尝试获取锁,具体通过 CAS 操作尝试批改 state 值,胜利则设置 state 值为 1,且设置对象独占锁线程为以后线程
2. 获取失败,创立一个 Node 节点绑定以后的线程,退出到一个 FIFO 的双向链表中
3. 如果持有锁的线程应用 tryRelease() 开释了锁,state 从新设置为 0,独占线程设置为 null,唤醒队列中的第一个 Node 节点中的线程再次争抢锁

共享模式流程:
1.tryRequireShared()办法尝试获取锁,具体通过判断以后 state 值,>0 则代表获取锁失败,= 0 则获取锁胜利
2. 获取失败,创立一个 Node 节点绑定以后的线程,退出到一个 FIFO 的双向链表中
3. 如果持有锁的线程应用 tryRelease() 开释了锁,会 state 进行 -1,当 state= 0 时,唤醒队列中所有的 Node 节点中的线程

参考大佬:
我画了 35 张图就是为了让你深刻 AQS
从 ReentrantLock 的实现看 AQS 的原理及利用

正文完
 0