之前的文章中也分类很多篇幅去介绍并发编程中的知识点,但相对分散,不够系统。
本文将是一个串联 & 汇总, 还原java中并发编程的全貌。
一、锁的分类
1.1 可重入锁、不可重入锁
synchronized
,ReentrantLock
,ReentrantReadWriteLock
都是可重入锁。
重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的。可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。
不可重入:当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到的,因为A锁被当前线 程占用着,需要等待自己释放锁再获取锁。不可重入锁也叫自旋锁。要实现不可重入锁,需要自定义一个锁类,并在其中禁止同一线程多次获取锁。
以synchronized为例说明下可重入锁是啥意思:
public class ReentrantExample {
public synchronized void method1() {
System.out.println("method1");
method2(); // 调用method2时,线程已经持有对象锁
}
public synchronized void method2() {
System.out.println("method2");
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
example.method1(); // 调用method1
}
}
在上面的代码中,method1和method2都是同步方法,当一个线程调用method1时,它会获取ReentrantExample对象的锁。在method1中,线程再次调用了method2,由于method2也是同步方法,线程需要再次获取对象的锁。因为sychronized是可重入的,线程可以再次获取锁并进入method2,不用重新获取锁,从而不会导致阻塞或死锁。
可重入原理:
synchronized的可重入性是由监视器Monitor锁的计数器来实现的(Monitor有个count属性就是计数用的)。当一个线程获取一个对象的锁时,锁的计数器会加1;当线程退出同步块或方法时,计数器会减1。如果计数器为0,则表示锁已被释放。同一个线程, 锁重入, 会对state进行自增. 释放锁的时候, state进行自减; 当state自减为0的时候. 此时线程才会将锁释放成功, 才会进一步去唤醒其他线程来竞争锁。
计数器的工作原理
- 第一次获取锁:当线程第一次进入synchronized块或方法时,监视器锁的计数器从0增加到1,并记录当前持有锁的线程ID。
- 再次获取锁:如果该线程在持有锁的情况下再次进入另一个synchronized块或方法,计数器会再次增加,表明线程多次获取了同一个锁。
- 锁的释放:每当线程退出synchronized块或方法时,计数器会减少。当计数器减少到0时,锁被真正释放,其他线程可以获取该锁。
1.2 乐观锁、悲观锁
synchronized
,ReentrantLock
,ReentrantReadWriteLock
都是悲观锁。
CAS
操作,就是乐观锁的一种实现。
悲观锁:获取不到锁资源时,会将当前线程挂起(进入BLOCKED、WAITING),线程挂起会涉及到用户态和内核态的切换,而这种切换是比较消耗资源的。
乐观锁:获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源。
Atomic原子性类中,就是基于CAS乐观锁实现的。
1.3 公平锁、非公平锁
synchronized
只能是非公平锁。
ReentrantLock
,ReentrantReadWriteLock
可以实现公平锁和非公平锁
公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,锁被A持有,同时线程B在排队。直接排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。
非公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,先尝试竞争一波拿到锁资源:开心,插队成功。没有拿到锁资源:依然要排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。
1.4 互斥锁、共享锁
synchronized
、ReentrantLock
是互斥锁。
ReentrantReadWriteLock
,有互斥锁也有共享锁。
互斥锁:同一时间点,只会有一个线程持有者当前互斥锁。
共享锁:同一时间点,当前共享锁可以被多个线程同时持有。
可以看到锁的分类有多个维度的多种类型,但是主要的锁就那么几个,下面我们将分别介绍。
二、synchronized的优化
synchronized的锁是基于对象实现的。
在JDK1.5的时候,Doug Lee推出了ReentrantLock
,lock的性能远高于synchronized
,所以JDK团队就在JDK1.6中,对synchronized
做了大量的优化。Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了。
2.1 锁消除
在synchronized
修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,即便写了synchronized
,他也不会触发。 之前说过的逃逸流程,若某个变量没有逃逸,那么就是线程安全的,就会触发同步省略。
2.2 锁膨胀
如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。
public void method(){
for(int i = 0;i < 999999;i++){
synchronized(对象){
}
}
// 这是上面的代码会触发锁膨胀
synchronized(对象){
for(int i = 0;i < 999999;i++){
}
}
}
2.3 锁升级
ReentrantLock
的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized
在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized
性能比较差。 synchronized
就在JDK1.6做了锁升级的优化:
- 无锁、匿名偏向:当前对象没有作为锁存在
- 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程
- 如果是,直接拿着锁资源走。
- 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
- 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源
- 如果成功获取到,拿着锁资源走
- 如果自旋了一定次数,没拿到锁资源,锁升级
- 重量级锁:就是最传统的
synchronized
方式,拿不到锁资源,就挂起当前线程
整个锁升级状态的转变:
综上所述,synchronized无论处理哪种锁,都是先尝试获取,获取不到才升级|| 放到队列上的,所以是非公平的。
2.4 synchronized实现原理
展开MarkWord
MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、重量级锁。
Monitor对象 :
ObjectMonitor() {
_header = NULL; // header存储着MarkWord
_count = 0; // 竞争锁的线程个数
_waiters = 0, // wait的线程个数
_recursions = 0; // 标识当前synchronized锁重入的次数
_object = NULL;
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // 保存wait的线程信息,双向链表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 获取锁资源失败后,线程要放到当前的单向链表中
FreeNext = NULL ;
_EntryList = NULL ; // _cxq以及被唤醒的WaitSet中的线程,在一定机制下,会放到EntryList中
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
Monitor
对象有三个集合属性:waitSet、EntryList、Owner
对象锁的基本工作机制:
当多个线程同时访问一段同步代码时,首先会进入 _EntryList队列中阻塞。
当某个线程获取到对象的对象锁后进入临界区域,并把对象锁中的 _owner变量设置为当前线程,即获得对象锁。
若持有对象锁的线程调用 wait() 方法,将释放当前持有的对象锁,_owner变量恢复为null,同时该线程进入 _WaitSet 集合中等待被唤醒。
在_WaitSet集合中的线程被唤醒,会被再次放到_EntryList队列中,重新竞争获取锁。
若当前线程执行完毕也将释放对象锁并复位变量的值,以便其他线程进入获取锁。
三、ReentrantLock
3.1 ReentrantLock和synchronized的区别
核心区别:
ReentrantLock
是个类(由AQS框架实现,JUC包,可以说是基于JDK实现的)synchronized
是关键字(可以说是基于JVM实现的)
效率区别:
- 如果竞争比较激烈,推荐
ReentrantLock
去实现,不存在锁升级概念。而synchronized
是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。
底层实现区别:
- 实现原理是不一样,
ReentrantLock
基于AQS实现的,synchronized
是基于ObjectMonitor
功能向的区别:
- ReentrantLock的功能比synchronized更全面。
- ReentrantLock支持公平锁和非公平锁
- ReentrantLock可以指定等待锁资源的时间
-
ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
-
ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
选择哪个:如果并发编程特别熟练,推荐使用ReentrantLock
,功能更丰富。如果掌握的一般般,使用synchronized
会更好。很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized。
四、AQS
AQS就是AbstractQueuedSynchronizer
抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock
,ThreadPoolExecutor
,阻塞队列,CountDownLatch
,Semaphore,CyclicBarrier
等等都是基于AQS实现。
AQS核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞+等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。(CLH队列的全称是Craig, Landin, and Hagersten lock queue。它是一种基于链表结构的自旋锁等待队列,用于存储被阻塞的线程信息)
- state:代表被抢占的锁的状态(valotile修饰,int类型)
- 队列:没有抢到锁的线程会包装成一个node节点存放到一个双向链表中
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
再具体一点:
4.1 独占和共享两种模式加锁流程
独占(锁只会被一个线程独占)和共享(多个线程可同时执行)。
独占锁加锁流程图:
共享锁的加锁 — acquireShared
4.2 加锁流程源码剖析
4.2.1 三种加锁源码分析(公平与非公平)
lock方法
执行lock方法后,公平锁和非公平锁的执行套路不一样
// 非公平锁
final void lock() {
// 上来就先基于CAS的方式,尝试将state从0改为1
if (compareAndSetState(0, 1))
// 获取锁资源成功,会将当前线程设置到exclusiveOwnerThread属性,代表是当前线程持有着锁资源
setExclusiveOwnerThread(Thread.currentThread());
else
// 执行acquire,尝试获取锁资源
acquire(1);
}
// 公平锁
final void lock() {
// 执行acquire,尝试获取锁资源
acquire(1);
}
acquire
方法,是公平锁和非公平锁的逻辑一样
public final void acquire(int arg) {
// tryAcquire:再次查看,当前线程是否可以尝试获取锁资源
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 没有拿到锁资源
// addWaiter(Node.EXCLUSIVE):将当前线程封装为Node节点,插入到AQS的双向链表的结尾
// acquireQueued:查看我是否是第一个排队的节点,如果是可以再次尝试获取锁资源,如果长时间拿不到,挂起线
// 如果不是第一个排队的额节点,就尝试挂起线程即可
// 中断线程的操作
selfInterrupt();
}
tryAcquire
方法竞争锁资源的逻辑,分为公平锁和非公平锁
// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取了state熟属性
int c = getState();
// 判断state当前是否为0,之前持有锁的线程释放了锁资源
if (c == 0) {
// 再次抢一波锁资源
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
// 拿锁成功返回true
return true;
}
}
// 不是0,有线程持有着锁资源,如果是,证明是锁重入操作
else if (current == getExclusiveOwnerThread()) {
// 将state + 1
int nextc = c + acquires;
if (nextc < 0) // 说明对重入次数+1后,超过了int正数的取值范围
// 01111111 11111111 11111111 11111111
// 10000000 00000000 00000000 00000000
// 说明重入的次数超过界限了。
throw new Error("Maximum lock count exceeded");
// 正常的将计算结果,复制给state
setState(nextc);
// 锁重入成功
return true;
}
// 返回false
return false;
}
// 公平锁实现
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// ....
int c = getState();
if (c == 0) {
// 查看AQS中是否有排队的Node
// 没人排队抢一手 。有人排队,如果我是第一个,也抢一手
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;
}
// 查看是否有线程在AQS的双向队列中排队
// 返回false,代表没人排队
public final boolean hasQueuedPredecessors() {
// 头尾节点
Node t = tail;
Node h = head;
// s为头结点的next节点
Node s;
// 如果头尾节点相等,证明没有线程排队,直接去抢占锁资源
// s节点不为null,并且s节点的线程为当前线程(排在第一名的是不是我)
return h != t && (s == null || s.thread != Thread.currentThread());
}
addWaite方法,将没有拿到锁资源的线程扔到AQS队列中去排队
// 没有拿到锁资源,过来排队, mode:代表互斥锁
private Node addWaiter(Node mode) {
// 将当前线程封装为Node,
Node node = new Node(Thread.currentThread(), mode);
// 拿到尾结点
Node pred = tail;
// 如果尾结点不为null
if (pred != null) {
// 当前节点的prev指向尾结点
node.prev = pred;
// 以CAS的方式,将当前线程设置为tail节点
if (compareAndSetTail(pred, node)) {
// 将之前的尾结点的next指向当前节点
pred.next = node;
return node;
}
}
// 如果CAS失败,以死循环的方式,保证当前线程的Node一定可以放到AQS队列的末尾
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
// 拿到尾结点
Node t = tail;
// 如果尾结点为空,AQS中一个节点都没有,构建一个伪节点,作为head和tail
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 比较熟悉了,以CAS的方式,在AQS中有节点后,插入到AQS队列的末尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued
方法,判断当前线程是否还能再次尝试获取锁资源,如果不能再次获取锁资源,或者又没获取到,尝试将当前线程挂起
// 当前没有拿到锁资源后,并且到AQS排队了之后触发的方法。 中断操作这里不用考虑
final boolean acquireQueued(final Node node, int arg) {
// 不考虑中断
// failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly)
boolean failed = true;
try {
boolean interrupted = false;
// 死循环............
for (;;) {
// 拿到当前节点的前继节点
final Node p = node.predecessor();
// 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。
if (p == head && tryAcquire(arg)) {
// 获取锁资源成功
// 设置头结点为当前获取锁资源成功Node,并且取消thread信息
setHead(node);
// help GC
p.next = null;
// 获取锁失败标识为false
failed = false;
return interrupted;
}
// 没拿到锁资源......
// shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true,
// 如果不能,就返回false,继续下次循环
// 这里基于Unsafe类的park方法,将当前线程挂起
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 这里基于Unsafe类的park方法,将当前线程挂起
interrupted = true;
}
} finally {
if (failed)
// 在lock方法中,基本不会执行。
cancelAcquire(node);
}
}
// 获取锁资源成功后,先执行setHead
private void setHead(Node node) {
// 当前节点作为头结点 伪
head = node;
// 头结点不需要线程信息
node.thread = null;
node.prev = null;
}
// 当前Node没有拿到锁资源,或者没有资格竞争锁资源,看一下能否挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// -1,SIGNAL状态:代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点
// 1,CANCELLED状态:代表当前节点以及取消了
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 上一个节点为-1之后,当前节点才可以安心的挂起线程
return true;
if (ws > 0) {
// 如果当前节点的上一个节点是取消状态,我需要往前找到一个状态不为1的Node,作为他的next节点
// 找到状态不为1的节点后,设置一下next和prev
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 上一个节点的状态不是1或者-1,那就代表节点状态正常,将上一个节点的状态改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
tryLock方法
// tryLock方法,无论公平锁还有非公平锁。都会走非公平锁抢占锁资源的操作
// 就是拿到state的值, 如果是0,直接CAS浅尝一下
// state 不是0,那就看下是不是锁重入操作
// 如果没抢到,或者不是锁重入操作,告辞,返回false
public boolean tryLock() {
// 非公平锁的竞争锁操作
return sync.nonfairTryAcquire(1);
}
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;
}
tryLock(time, unit)
// tryLock(time,unit)执行的方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {
// 线程的中断标记位,是不是从false,别改为了true,如果是,直接抛异常
if (Thread.interrupted())
throw new InterruptedException();
// tryAcquire分为公平和非公平锁两种执行方式,如果拿锁成功, 直接告辞,
return tryAcquire(arg) ||
// 如果拿锁失败,在这要等待指定时间
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果等待时间是0秒,直接告辞,拿锁失败
if (nanosTimeout <= 0L)
return false;
// 设置结束时间。
final long deadline = System.nanoTime() + nanosTimeout;
// 先扔到AQS队列
final Node node = addWaiter(Node.EXCLUSIVE);
// 拿锁失败,默认true
boolean failed = true;
try {
for (;;) {
// 如果在AQS中,当前node是head的next,直接抢锁
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 结算剩余的可用时间
nanosTimeout = deadline - System.nanoTime();
// 判断是否是否用尽的位置
if (nanosTimeout <= 0L)
return false;
// shouldParkAfterFailedAcquire:根据上一个节点来确定现在是否可以挂起线程
// 避免剩余时间太少,如果剩余时间少就不用挂起线程
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
// 如果剩余时间足够,将线程挂起剩余时间
LockSupport.parkNanos(this, nanosTimeout);
// 如果线程醒了,查看是中断唤醒的,还是时间到了唤醒的。
if (Thread.interrupted())
// 是中断唤醒的!
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
unlock()
public void unlock() {
// 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
sync.release(1);
}
// 释放锁的核心流程
public final boolean release(int arg) {
// 核心释放锁资源的操作之一
if (tryRelease(arg)) {
// 如果锁已经释放掉了,走这个逻辑
Node h = head;
// h不为null,说明有排队的(录课时估计脑袋蒙圈圈。)
// 如果h的状态不为0(为-1),说明后面有排队的Node,并且线程已经挂起了。
if (h != null && h.waitStatus != 0)
// 唤醒排队的线程
unparkSuccessor(h);
return true;
}
return false;
}
// ReentrantLock释放锁资源操作
protected final boolean tryRelease(int releases) {
// 拿到state - 1(并没有赋值给state)
int c = getState() - releases;
// 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// free,代表当前锁资源是否释放干净了。
boolean free = false;
if (c == 0) {
// 如果state - 1后的值为0,代表释放干净了。
free = true;
// 将持有锁的线程置位null
setExclusiveOwnerThread(null);
}
// 将c设置给state
setState(c);
// 锁资源释放干净返回true,否则返回false
return free;
}
// 唤醒后面排队的Node
private void unparkSuccessor(Node node) {
// 拿到头节点状态
int ws = node.waitStatus;
if (ws < 0)
// 先基于CAS,将节点状态从-1,改为0
compareAndSetWaitStatus(node, ws, 0);
// 拿到头节点的后续节点。
Node s = node.next;
// 如果后续节点为null或者,后续节点的状态为1,代表节点取消了。
if (s == null || s.waitStatus > 0) {
s = null;
// 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点环境
for (Node t = tail; t != null && t != node; t = t.prev)
// 从后往前找到状态小于等于0的节点
// 找到离head最新的有效节点,并赋值给s
if (t.waitStatus <= 0)
s = t;
}
// 只要找到了这个需要被唤醒的节点,执行unpark唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
4.3 Condition
Condition是用来替代传统的0bject的wait()、notify()实现现线程间的协作,使用Condition中的await()、signal()实现线程间协作更加安全和高效。
比如,这里线程一先获取锁,然后使用 await()方法挂起当前线程并释放锁,线程二获取锁后使用signal 唤醒线程一:
AbstractQueueSynchronizer 中实现了 Condition 中的方法,主要对外提供awaite(object.wait())和 signal(object.notify())调用。
五、
CountDownLatch
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。
它使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,在CountDownLatch上 await() 的线程就会被唤醒,就可以恢复执行任务。
所以,CountDownLatch不是完美的,它是一次性的,计数器的值只能在初始化时构造一次,之后没有任何机制能够对它重设值。也就是说,当CountDownLatch使用完毕之后不能再次使用。
典型用法1
一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为 n
CountDownLatch countDownLatch = new CountDownLatch(n)
每当一个任务线程执行完毕,就将计数器减1
countdownlatch.countDown()
当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。
public class CountdownLatchTest1 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
final CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println("子线程" + Thread.currentThread().getName() + "开始执行");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("子线程" + Thread.currentThread().getName() + "执行完成");
latch.countDown();//当前线程调用此方法,则计数减一
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
try {
System.out.println("主线程" + Thread.currentThread().getName() + "等待子线程执行完成...");
latch.await();//阻塞当前线程,直到计数器的值为0
System.out.println("主线程" + Thread.currentThread().getName() + "开始执行...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
典型用法2
实现多个线程开始执行任务的最大并行性。
类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。await() 方法使当前线程在 CountDownLatch 计数减至零之前一直等待,除非线程被中断。
举例一:
private final static int threadCount = 200;
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
test(threadNum);
} catch (Exception e) {
LOGGER.warning("execption:" + e);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
// countDownLatch.await(10, TimeUnit.MILLISECONDS); // 指定时间内完成
LOGGER.info("finish !!!");
exec.shutdown();
}
......
七月 12, 2018 5:17:51 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 7
七月 12, 2018 5:17:51 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 6
七月 12, 2018 5:17:51 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 4
七月 12, 2018 5:17:51 下午 com.snowalker.test.aqs.CountDownLatchDemo main
信息: finish !!!
指定了计数器为200,让主线程await,等待200个线程执行完毕。每个子线程在执行完毕都对计数器进行减一操作。当200个子线程都执行完毕,主线程才执行。
如果我们不想一直等待,可以指定线程等待时间,通过
countDownLatch.await(10, TimeUnit.MILLISECONDS); // 指定时间内完成
七月 12, 2018 5:18:36 下午 com.snowalker.test.aqs.CountDownLatchDemo main
信息: finish !!!
七月 12, 2018 5:18:36 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 5
七月 12, 2018 5:18:36 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 4
七月 12, 2018 5:18:36 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 7
我指定为10毫秒,可以看到我们的主线程没有等待所有线程执行完毕才被唤醒,而是等待10毫秒后恢复执行,输出finish!!!
举例二:
public class CountdownLatchTest2 {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
final CountDownLatch cdOrder = new CountDownLatch(1);
final CountDownLatch cdAnswer = new CountDownLatch(4);
for (int i = 0; i < 4; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("选手" + Thread.currentThread().getName() + "正在等待裁判发布口令");
cdOrder.await();
System.out.println("选手" + Thread.currentThread().getName() + "已接受裁判口令");
Thread.sleep((long) (Math.random() * 10000));
System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
cdAnswer.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
service.execute(runnable);
}
try {
Thread.sleep((long) (Math.random() * 10000));
System.out.println("裁判"+Thread.currentThread().getName()+"即将发布口令");
cdOrder.countDown();
System.out.println("裁判"+Thread.currentThread().getName()+"已发送口令,正在等待所有选手到达终点");
cdAnswer.await();
System.out.println("所有选手都到达终点");
System.out.println("裁判"+Thread.currentThread().getName()+"汇总成绩排名");
} catch (InterruptedException e) {
e.printStackTrace();
}
service.shutdown();
}
}
CountDownLatch核心实现类是Sync,它是一个继承自AbstractQueuedSynchronizer的内部类。
// Sync 类继承自 AbstractQueuedSynchronizer,提供了共享锁的功能
private static final class Sync extends AbstractQueuedSynchronizer {
// 构造方法,设置初始计数值
Sync(int count) {
// 使用 AQS 的 setState 方法来设置同步器的状态,这里用 count 来初始化状态
// 计数值即锁的可用次数
setState(count);
}
// 获取当前的计数值(即剩余的共享资源数量)
int getCount() {
// 调用 AQS 的 getState 获取当前状态,即共享资源的剩余数量
return getState();
}
// 试图以共享模式获取锁(acquireShared),即检查是否可以获取共享资源
protected int tryAcquireShared(int acquires) {
// 状态为0时,返回为1,表示资源获取成功。返回为-1时,表示资源不可用,线程需要挂起等待。
return (getState() == 0) ? 1 : -1;
}
// 试图以共享模式释放锁(releaseShared),即释放一个共享资源
protected boolean tryReleaseShared(int releases) {
for (;;) { // 循环尝试,直到成功
// 获取当前的状态值(即共享资源数量)
int c = getState();
//如果当前状态(即共享资源的数量)为 0,表示已经没有共享资源可以释放,因此返回 false,表示释放操作失败。
if (c == 0)
return false; // 没有资源,返回 false
// 计数器的值减1
int nextc = c - 1;
// 尝试更新状态(使用 CAS 操作)
if (compareAndSetState(c, nextc)) {
// 如果计数器的值减为0,说明所有线程已经执行完毕,返回 true,否则返回false。
return nextc == 0;
}
}
}
}
六、
CyclicBarrier
CyclicBarrier可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。
举个例子,比如小明,小美,小华,小丽几人相约一起聚餐,然而他们每个人到达约会地点的耗时都不一样,有的人会早到,有的人会晚到,但是他们要都到了以后才可以决定点那些菜。这里每个人相当于一个线程,而餐厅就是 CyclicBarrier。
CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。
使用举例:
public class CyclicBarrierDemo1 {
public static CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
public static class T extends Thread {
int sleep;
public T(String name, int sleep) {
super(name);
this.sleep = sleep;
}
@Override
public void run() {
try {
//模拟休眠
TimeUnit.SECONDS.sleep(sleep);
long starTime = System.currentTimeMillis();
//调用await()的时候,当前线程将会被阻塞,需要等待其他员工都到达await了才能继续
cyclicBarrier.await();
long endTime = System.currentTimeMillis();
System.out.println(this.getName() + ",sleep:" + this.sleep + " 等待了" + (endTime - starTime) + "(ms),开始吃饭了!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 10; i++) {
new T("员工" + i, i).start();
}
}
}
员工10,sleep:10 等待了0(ms),开始吃饭了!
员工1,sleep:1 等待了8996(ms),开始吃饭了!
员工7,sleep:7 等待了2998(ms),开始吃饭了!
员工9,sleep:9 等待了1000(ms),开始吃饭了!
员工6,sleep:6 等待了3997(ms),开始吃饭了!
员工4,sleep:4 等待了6000(ms),开始吃饭了!
员工5,sleep:5 等待了4999(ms),开始吃饭了!
员工2,sleep:2 等待了7999(ms),开始吃饭了!
员工3,sleep:3 等待了7002(ms),开始吃饭了!
员工8,sleep:8 等待了1999(ms),开始吃饭了!
CyclicBarrier和CountDownLatch的区别
CountDownLatch 主要用来解决一个线程等待多个线程的场景,可以类比旅游团团长要等待所有游客到齐才能去下一个景点。
而 CyclicBarrier 是一组线程之间的相互等待,可以类比几个驴友之间的不离不弃,共同到达某个地方,再继续出发,这样反复。
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
- CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
- CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
- 都可以用于“主线程阻塞一直等待,直到子任务完成,主线程才继续执行”的情况。
七、Semaphore
[ˈseməfɔːr]
其实可以将Semaphore看成一个计数器,当计数器的值小于许可最大值时,所有调用acquire方法的线程都可以得到一个许可从而往下执行。而调用release方法则可以让计数器的值减一。
信号量的主要应用场景是控制最多N个线程同时地访问资源,其中计数器的最大值即是许可的最大值N。以停车场为例,假设停车场一共有8个车位,其中6个车位已被停放,然后来了两辆汽车,此时因为刚好剩下两个车位所以这两辆车都能停放。接着又来了一辆车,现在已经没有空位了所以只能等待其它车离开。此时刚好一辆红色汽车离开停车场,来开后黄车刚好可以停进去,假如又有一辆汽车进来则该车又得等待。如此往复。这个过程中停车场就是公共资源,车位数就是信号量最大许可数,车辆就好比线程。
举例:
import java.util.Random;
import java.util.concurrent.Semaphore;
public class MyThread extends Thread {
private final Semaphore semaphore;
private final Random random = new Random();
public MyThread(String name, Semaphore semaphore) {
super(name);
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " - 抢座成功,开始写作业");
Thread.sleep(random.nextInt(1000));
System.out.println(Thread.currentThread().getName() + " - 作业完成,腾出座位");
} catch (InterruptedException e) {
e.printStackTrace();
}
semaphore.release();
}
}
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 5; i++) {
new MyThread("学⽣-" + (i + 1), semaphore).start();
}
}
学⽣-1 - 抢座成功,开始写作业
学⽣-2 - 抢座成功,开始写作业
学⽣-2 - 作业完成,腾出座位
学⽣-3 - 抢座成功,开始写作业
学⽣-1 - 作业完成,腾出座位
学⽣-4 - 抢座成功,开始写作业
学⽣-3 - 作业完成,腾出座位
学⽣-5 - 抢座成功,开始写作业
学⽣-5 - 作业完成,腾出座位
学⽣-4 - 作业完成,腾出座位
上面主方法中,调用new Semaphore(2)
定义了2份共享资源,或者两份许可证,也就是同一时刻最多只允许2个线程执行。
Semaphore 基于 AbstractQueuedSynchronizer
(AQS) 实现,其实现方式和 CountDownLatch
类似,但使用了 AQS 的共享模式,并对许可计数进行精确管理。其中 AQS 的 state
表示剩余许可数。acquireShared()
方法用于申请许可,releaseShared()
方法用于释放许可。CountDownLatch的实现同样基于AQS,但其计数器只能递减到零,一旦计数器为零,所有等待的线程将被唤醒。CountDownLatch的核心方法包括await()
和countDown()
,分别用于等待所有操作完成和减少计数器的值
Semaphore 的核心实现依赖于其内部类 Sync
,Sync 是 AbstractQueuedSynchronizer
的子类。根据是否是公平模式,有两种实现:NonfairSync
和 FairSync
。
- NonfairSync:非公平模式。线程调用
acquire
时直接尝试获取许可,不保证顺序。 - FairSync:公平模式。线程获取许可遵循队列顺序。
abstract static class Sync extends AbstractQueuedSynchronizer {
// 设置初始许可数
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
// 加锁state许可减一
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
// 释放锁state许可加一
int next = current + releases;
if (compareAndSetState(current, next))
return true;
}
}
}
八、ReentrantReadWriteLock
读写锁的内部包含两把锁:一把是读锁,是一种共享锁;另一把是写锁,是一种独占锁
- 在没有写锁的时候,读锁可以被多个线程同时持有
- 写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞,抢占读锁也会阻塞
读写互斥原则:
- 读读相容
- 读写互斥
- 写写互斥
JUC包中的读写锁接口为ReadWriteLock:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();//返回读锁
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();//返回写锁
}
ReentrantReadWriteLock类图
WriteLock、Sync、ReadLock、FairSync、NonfaruSyns都是ReadWritrLock的静态内部类。
AQS 中只维护了 一个 state 状态,而 ReentrantReadWriteLock 则 需要维护读状态和写状态:用 state 的高16 位表示读状态,也就是获取到读锁的次数;使用低 16 位表示获取到写锁的线程的可重入次数 。
static final int SHARED_SHIFT = 16;
//共享锁(读锁)状态单位值 65536 1<<16 2^16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//共享锁线程最大个数 65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//排它锁(写锁)掩码,二进制,15 个 1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//返回读锁线程数
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//返回写锁可重入个数
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
//firstReader 用来记录第一个获取到读锁的线程
private transient Thread firstReader = null;
//firstReaderHoldCount 则记录第 一个获取到读锁的线程获取读锁的可重入次数
private transient int firstReaderHoldCount;
//cachedHoldCounter 用来记录最后 一个获取读锁的线程获取读锁 的可重入次数
private transient HoldCounter cachedHoldCounter;
8.1 写锁的获取和释放
ReentrantReadWriteLock 中 写锁使用 WriteLock 来实现
public static class WriteLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
}
//AbstractQueuedSynchronizer类
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//Sync类
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
//c!=0说明读锁或者写锁已经被某线程获取
if (c != 0) {//代码1处
// (Note: if c != 0 and w == 0 then shared count != 0)
// w=O说明已经有线程获取了读锁
// w!=O 并且当前线程不是写锁拥有者则返回false
if (w == 0 || current != getExclusiveOwnerThread())//代码2处
return false;
//说明当前线程获取了写锁,判断可重入次数
if (w + exclusiveCount(acquires) > MAX_COUNT)//代码3处
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);//设置可重入次数 代码4处
return true;
}
//第 一个写线程获取写锁
//代码5处
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
tryAcquire方法中:
- 如果当前 AQS 状态值不为 0 则说明 当前己经有线程获取到了读锁或者写锁。
- 如果 w==0 说明状态值 的低16 位为0(低16位表示写状态) ,而 AQS 状态值不为 0,则说明高16位(高16位表示读状态)不为 0 ,这暗示己经有线程获取了读锁 ,所以直接返回 false
- 如果 w! =0 则说明 当前已经有线程获取了该写锁,再看当前线程是不是该锁的持有者 ,如果不是则返回 false
- 若当前线程之前已经获取到了该锁,判断该线程可重入次数是否超过了最大值,是则抛异常
- 增加当前线程的可重入次数,返回true
- AQS 的状态值等于 0则说明目前没有线程获取到读锁和写锁,让第 一个写线程获取写锁
释放锁
//WriteLock类
public void unlock() {
sync.release(1);
}
//AQS类
public final boolean release(int arg) {
//调用ReentrantReadWriteLock中sync类实现的tryRelease方法
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)//激活阻塞队列里面的一个线程
unparkSuccessor(h);
return true;
}
return false;
}
//Sync类
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())//看是否是写锁拥有者调用的unlock
throw new IllegalMonitorStateException();
//获取可重入值,这里没有考虑高16位,因为获取写锁时读锁状态值肯定为0
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)//如采写锁可重入值为0则释放锁,否则只是简单地更新状态值
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
- 如果当前线程持有该锁,调用该方法会让该线程对该线程持有的 AQS状态值减1
- 如果减去1后当前状态值为0则当前线程会释放该锁 ,否则仅仅减1而己
- 如果当前线程没有持有该锁而调用了该方法则 会抛出 Illega!MonitorStateException 异常
8.2 读锁的获取和释放
ReentrantReadWri teLock 中的读锁是使用 ReadLock 来实现的。
//ReadLock类
public void lock() {
sync.acquireShared(1);
}
//AQS类
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//java.util.concurrent.locks.ReentrantReadWriteLock.Sync.tryAcquireShared(int)
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();//获取当前状态值 代码1
//代码2处
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)//判断是否写锁被占用
return -1;
int r = sharedCount(c);//获取读锁计数 代码3处
//尝试获取锁 ,多个读线程只有一个会成功,不成功的进入fullTryAcquireShared进行重试
if (!readerShouldBlock() && //代码4处
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {//第 一个线程获取读锁 代码5处
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {//如果当前线程是第一个获取读锁的线程 代码6处
firstReaderHoldCount++;
} else {
//记录最后一个获取读锁的线程或记录其他线程读锁的可重入数
HoldCounter rh = cachedHoldCounter; //代码7处
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);//代码8处
}
tryAcquireShared方法解释:
- 1. 代码1处首先获取了当前 AQS 的状态值
- 2. 代码2处查看是否有其他线程获取到了写锁,如果是则直接返回-1
- 3. 代码3处,得到获取到的读锁的个数 , 到这里说明目前没有线程获取到写锁 ,但是可能有线程持有读锁
- 4. 代码4处的中非公平锁的 readerShouldBlock 实现代码如下:
//java.util.concurrent.locks.ReentrantReadWriteLock.NonfairSync.readerShouldBlock()
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer.apparentlyFirstQueuedIsExclusive()
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
如果队列里面存在一个元素,则判断第一个元素是不是正在尝试获取写锁,如果不是(!readerShouldBlock()=true),则当前线程判断当前获取读锁的线程是否达到了最大值( r < MAX_COUNT)。 最后执行CAS 操作将 AQS 状态值的高 16 位值增1(compareAndSetState(c, c + SHARED_UNIT)) 。
- 5. 代码5处和代码6处记录第一个获取读锁的线程并统计该线程获取读锁的可重入数
- 6. 代码7处使用 cachedHoldCounter 记录最后一个获取到读锁的线程和该线程获取读锁的可重入数
- readHolds记录了当前线程获取读锁的可重入数
- 7. 如果可以到代码8处,说明readerShouldBlock()=true,说明有线程正在获取写锁,fullTryAcquireShared的代码与tryAcquireShared 类似(尝试获取共享锁),它们的不同之处在于,前者通过循环自旋获取
8.3 使用案例
补充一、怎么实现公平和非公平锁?
公平锁可以把竞争的线程放在一个先进先出的队列上,只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁就好了。
非公平锁是后到的线程可能比前到临界区的线程获取得到锁,线程先尝试能不能获取得到锁,如果获取得到锁了就执行同步代码了;如果获取不到锁,那就再把这个线程放到队列。
所以公平和非公平的区别就是:线程执行同步代码块时,是否会去尝试获取锁。如果会尝试获取锁,那就是非公平的。如果不会尝试获取锁,直接进队列,再等待唤醒,那就是公平的。
补充二、共享锁 源码
AQS 共享锁主要 API
- tryAcquireShared() 获取共享锁
- tryAcquireSharedNanos() 获取超时可中断共享锁
- releaseShared() 释放共享锁
tryAcquireShared():
// AQS的acquireShared方法实现
public final void acquireShared(int arg) {
// 尝试获取共享资源,返回值大于等于0表示成功,小于0表示失败
if (tryAcquireShared(arg) < 0) {
// 获取失败,则加入到等待队列中
doAcquireShared(arg);
}
}
// 子类需要重写此方法以实现具体的获取共享资源的逻辑
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 将当前线程加入到等待队列中
private void doAcquireShared(int arg) {
// 添加到队列的节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头节点,尝试再次获取共享资源
if (p == head) {
// 尝试加锁,假设成功了,肯定是返回0或者是大于0的正数
// 如果是0,加锁成功,该线程出队
int r = tryAcquireShared(arg);
if (r >= 0) {
// 获取成功,将当前节点设置为头节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// shouldParkAfterFailedAcquire表示是否应该将线程挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
throw new InterruptedException();
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
看到上述代码,可以和独占锁的代码对比下,非常相近,无非就有两个地方不一样,如下图示:
共享锁模式下调用的是 setHeadAndPropagate() ,这个方法主要是会去唤醒其他 CLH 中排队的线程。
private void setHeadAndPropagate(Node node, int propagate) {
// 将 AQS 的 head 赋值给 h 临时变量放着,后面要使用
Node h = head;
// 主要是把 AQS 的 head 头指针往后移动,表示有 Node 节点出队列。
setHead(node);
// 前面提到的 Doug Lea 大佬对 方法 tryAcquireShared() 规定了返回值,负数,和 0 、大于0的正数
// propagate 就是这个方法的返回值,如果你在子类实现这个 tryAcquireShared() 方法返回大于 0 的正数
// 这下面的 if 逻辑就直接进入,后面的判断都不会走了。
// 返回大于0的正数,表示当前线程可以加锁成功,而且还会通知其他线程也过来尝试加这把锁,下面就是干了这个事情
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// s == null 在并发场景下,还没有来得及给 node 后驱指向赋值,不就是 null 嘛
// 然后要判断该节点是否为共享模式还是独占模式,在 addWaiter() 方法已经封装了这个属性,这里就用到了
if (s == null || s.isShared())
// 看后面代码分析,摘到外面去分析了,看下面,看完之后回来
doReleaseShared();
}
}
// 主要是把 AQS 的 head 头指针往后移动,表示有 Node 节点出队列。
private void setHead(Node node) {
// 将 AQS 的 head 指向了当前线程运行的 Node 节点
head = node;
// 因为 Node 封装的线程已经加锁成功了,自然而然要把 thread 置成 null 值
node.thread = null;
// 同时也要把 Node 的前驱指向断开,方便上一个完成使命的哨兵 GC 回收
node.prev = null;
}
releaseShared()
public final boolean releaseShared(int arg) {
// 假设这里释放共享锁成功
if (tryReleaseShared(arg)) {
// 释放成功,后面肯定是要去看看唤醒阻塞挂起的线程呗,叫他们醒来过来加锁了
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
// 又是一个自旋,肯定有个地方退出
for (;;) {
// 释放都是从 head 开始找
Node h = head;
// 该节点肯定不是哨兵节点,也不是尾节点,从中间开始
if (h != null && h != tail) {
// 正常情况,被阻塞挂起的线程waitStatus 都是-1
int ws = h.waitStatus;
// 所以这里成立
if (ws == Node.SIGNAL) {
// 死命的去把 waitStatus 恢复成线程初始状态 0,直到成功,否则一直在这里转转
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 发现和加锁成功之后调用的方法竟然是一样的,也就是去唤醒后面的线程,这里不过多阐述了
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
tryAcquireSharedNanos()
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 系统时间加上超时时间就是时间临界点
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// nanosTimeout 表示剩余时间够不够超时时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
// 这里注意下,如果你设置的超时时间很多,都已经大于了系统默认 1000 ns 时间
// 那系统肯定不会傻乎乎的在这里用你的超时时间疯狂自旋,会用自己的默认时间作为超时时间
// 然后超过了,就去挂起不会去空转。这是个优化的地方,超时时间不要比系统默认的 1000ns 长
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 如果修改了线程中断标识,那么会被阻塞线程会响应你的请求,直接给你一个意外异常,然后退出for循环
// 但是会执行 finally 逻辑哦,注意
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
// 主要是去维护
cancelAcquire(node);
}
}
private void cancelAcquire(Node node) {
// 首先判断被中断的线程 Node 是否为 null
if (node == null)
return;
// 因为该 Node 是被中断然后结束退出了,所以 Node 的 Thread 属性自然而然就是 null
node.thread = null;
// 下面这个 while 循环是维护 CLH 队列完整性,把取消的队列踢出队列
Node pred = node.prev;
// 循环去找,知道找到有一个前驱节点不被取消的
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// 因为该 Node 被中断退出了,所以 waitStatus 设置成取消状态
node.waitStatus = Node.CANCELLED;
// 如果改 Node 就是尾节点,那就直接踢出即可
// 把 AQS 的 tail 指向该 Node 的前驱节点即可,该 Node 的前驱节点通过上面的 while
// 循环已经保证了这个 pred 是正常状态的
if (node == tail && compareAndSetTail(node, pred)) {
// 然后 pred 的后驱指针赋值为 null,因为 pred 作为了新的尾节点,后面没有节点了
compareAndSetNext(pred, predNext, null);
} else {
// 假设不是尾节点,那就说明该 Node 为中间节点然后被中断退出了
int ws;
// 首先排除哨兵节点,假设不是 head 节点,继续做后面的判断
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 假设进来了,该 Node 节点就要被踢出队列了,然后重新维护 CLH 队列完整性
// 保存该 Node 节点的后驱指针
Node next = node.next;
// 如果后驱节点不为 null,说明该 Node 存在后节点而且 waitStatus 是正常的,正常值 0或者-1
if (next != null && next.waitStatus <= 0)
// 把 pred 该 Node 的前驱节点,predNext 为该 Node 的后驱节点
// 然后这里直接把 pred 的后驱指针指向了 predNext,把 Node 直接踢出 CLH 队列了。
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
// 把该 Node 的后驱节点指向自己
node.next = node; // help GC
}
}
- 正常情况下,独占锁只有持有锁的线程运行完了,释放锁了,独占锁才会出队列
- 共享锁是唤醒下一个节点,并且,下一个节点成功设置成新的 head 头节点,自己就出队列
- 独占锁是只有在释放锁的时候,才会去看看要不要唤醒下一个节点,而共享锁在两个地方去看看要不要唤醒下一个节点,一个是在获取锁成功时,去调用 setHeadAdnPropagate() 方法尝试是否唤醒下一个线程,另一个是在释放共享锁成功之后会去唤醒线程。
补充三、AbstractOwnableSynchronizer(AOS)
在JDK1.6时发布,主要用于表示持有者与锁之间的关系。
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
protected AbstractOwnableSynchronizer() { }
//私有的不会被序列化的独占thread
private transient Thread exclusiveOwnerThread;
//set
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
//get
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
由于AOS是一个抽象类不能直接实例化,我们定义一个子类实例化:
public class AosClient extends AbstractOwnableSynchronizer {
public static void main(String[] args) {
AosClient client = new AosClient();
client.setExclusiveOwnerThread(Thread.currentThread());
Thread exclusiveOwnerThread = client.getExclusiveOwnerThread();
System.out.println(exclusiveOwnerThread);
}
}