不看源码就硬聊AQS实现原理

WillLiaowh

看了下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()

image.png
看看非公平锁的实现:
image.png
核心流程从这里开始:

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

2.acquire(1):若利用CAS尝试抢占锁失败,也就是获取锁失败,则进入Acquire方法进行后续处理
Acquire方法实现:
image.png
1.tryAcquire():再次尝试获取锁,如果加锁成功则返回true,不再执行以下步骤,否则继续执行以下步骤
2.addWaiter():走到这里说明加锁失败,创建一个Node节点绑定当前的线程,加入到一个FIFO的双向链表中,然后返回这个Node
3.acquireQueued():这个方法会先判断当前传入的Node对应的前置节点是否为head节点,如果是则尝试加锁,如果加锁失败或者Node的前置节点不是head节点,用LockSupport.park()挂起当前线程。
上述流程图:
image.png

unlock()

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

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

非公平锁和公平锁

线程二释放锁的时候,唤醒被挂起的线程三线程三执行tryAcquire()方法使用CAS操作来尝试修改state值,如果此时又来了一个线程四也来执行加锁操作,同样会执行tryAcquire()方法。这种情况就会出现竞争,线程四如果获取锁成功,线程三仍然需要待在等待队列中被挂起。这就是所谓的非公平锁线程三辛辛苦苦排队等到自己获取锁,却眼巴巴的看到线程四插队获取到了锁。
非公平锁执行流程:
image.png
公平锁在加锁的时候,会先判断AQS等待队列中是存在节点,如果存在其他等待线程,那么自己也会加入到等待队列尾部,做到真正的先来后到,有序加锁。
公平锁执行流程:
image.png

非公平锁和公平锁的区别:
非公平锁性能高于公平锁性能。非公平锁可以减少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执行");
    }

初始化

image.png
初始化state值,当state值>0代表锁被占有,=0说明锁被释放

await()

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

countDown()

image.png
image.png
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的原理及应用

阅读 115

我要去大厂
世界上最伟大的力量是坚持。

世界上最伟大的力量是坚持。

37 声望
7 粉丝
0 条评论
你知道吗?

世界上最伟大的力量是坚持。

37 声望
7 粉丝
宣传栏