看了下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的原理及利用