看了下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节点中的线程