|
|
|
|
# 一、synchronized
|
|
|
|
|
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
|
|
|
|
|
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
|
|
|
|
|
```java
|
|
|
|
|
class SyncLock implements Runnable{
|
|
|
|
|
@Override
|
|
|
|
|
public void run() {
|
|
|
|
|
synchronized (this){
|
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
|
|
|
try {
|
|
|
|
|
System.out.println("线程名:"+Thread.currentThread().getName() + ":" + (count++));
|
|
|
|
|
Thread.sleep(100);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
synchronized (this)锁定的是当前对象,如果new一个其他对象的话,会生成新锁。
|
|
|
|
|
|
|
|
|
|
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
|
|
|
|
|
```java
|
|
|
|
|
public synchronized void run() {
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
|
|
|
try {
|
|
|
|
|
System.out.println("线程名:"+Thread.currentThread().getName() + ":" + (count++));
|
|
|
|
|
Thread.sleep(100);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
在用synchronized修饰方法时要注意以下几点:
|
|
|
|
|
+ synchronized关键字不能继承
|
|
|
|
|
+ 在定义接口方法时不能使用synchronized关键字
|
|
|
|
|
+ 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步
|
|
|
|
|
|
|
|
|
|
3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
|
|
|
|
|
静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。
|
|
|
|
|
```java
|
|
|
|
|
class SyncThread implements Runnable {
|
|
|
|
|
private static int count;
|
|
|
|
|
|
|
|
|
|
public SyncThread() {
|
|
|
|
|
count = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public synchronized static void method() {
|
|
|
|
|
for (int i = 0; i < 5; i ++) {
|
|
|
|
|
try {
|
|
|
|
|
System.out.println(Thread.currentThread().getName() + ":" + (count++));
|
|
|
|
|
Thread.sleep(100);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public synchronized void run() {
|
|
|
|
|
method();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
假如创建了2个SyncThread对象,但是由于有静态方法锁且锁属于这个类,无论创建多少对象,run()方法的锁依然生效。
|
|
|
|
|
|
|
|
|
|
4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
|
|
|
|
|
```java
|
|
|
|
|
class SyncThread implements Runnable {
|
|
|
|
|
private static int count;
|
|
|
|
|
|
|
|
|
|
public SyncThread() {
|
|
|
|
|
count = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void method() {
|
|
|
|
|
synchronized(SyncThread.class) {
|
|
|
|
|
for (int i = 0; i < 5; i ++) {
|
|
|
|
|
try {
|
|
|
|
|
System.out.println(Thread.currentThread().getName() + ":" + (count++));
|
|
|
|
|
Thread.sleep(100);
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public synchronized void run() {
|
|
|
|
|
method();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
效果与修饰静态方法相同
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 二、volatile
|
|
|
|
|
不能用于自增操作,例如count ++
|
|
|
|
|
作用如下:
|
|
|
|
|
1. 保证可见性,不保证原子性
|
|
|
|
|
当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
|
|
|
|
|
这个写会操作会导致其他线程中的volatile变量缓存无效。
|
|
|
|
|
2. 禁止指令重排
|
|
|
|
|
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果
|
|
|
|
|
|
|
|
|
|
假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。
|
|
|
|
|
![[Snipaste_2023-02-28_13-09-43 2.png]]
|
|
|
|
|
|
|
|
|
|
当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。
|
|
|
|
|
![[Snipaste_2023-02-28_13-09-43 3.png]]
|
|
|
|
|
|
|
|
|
|
从横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发送的消息。既然是旧的了,那线程B该怎么办了?自然而然就只能去主内存去取啦。
|
|
|
|
|
|
|
|
|
|
# 三、ReentrantLock
|
|
|
|
|
```java
|
|
|
|
|
public class ReentrantLockDemo {
|
|
|
|
|
// 实例化一个非公平锁,构造方法的参数为true表示公平锁,false为非公平锁。
|
|
|
|
|
private final ReentrantLock lock = new ReentrantLock(false);
|
|
|
|
|
private int i;
|
|
|
|
|
|
|
|
|
|
public void testLock() {
|
|
|
|
|
// 拿锁,如果拿不到会一直等待
|
|
|
|
|
lock.lock();
|
|
|
|
|
try {
|
|
|
|
|
// 再次尝试拿锁(可重入),拿锁最多等待100毫秒
|
|
|
|
|
if (lock.tryLock(100, TimeUnit.MILLISECONDS))
|
|
|
|
|
i++;
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
} finally {
|
|
|
|
|
// 释放锁
|
|
|
|
|
lock.unlock();
|
|
|
|
|
lock.unlock();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
代码中创建了非公平锁,lock.lock()会进行拿锁操作,如果拿不到锁则会一直等待。如果拿到锁之后则会执行try代码块中的代码。接下来在try代码块中又通过tryLock(100, TimeUnit.MILLISECONDS)方法尝试再次拿锁,此时,拿锁最多会等待100毫秒,如果在100毫秒内能获得锁,则tryLock方法返回true,拿锁成功,执行i++操作,如果返回false,获取锁失败,i++不会被执行。(因为第一次线程已经拿到锁了,由于ReentrantLock是可重入,因此,第二次必定能拿到锁。此处仅仅为了演示ReetranLock的使用,不必纠结)。
|
|
|
|
|
另外,要注意被ReentrantLock加锁区域必须用try代码块包裹,且释放锁需要在finally中来避免死锁。执行几次加锁,就需要几次释放锁。
|
|
|
|
|
|
|
|
|
|
## 3.1 公平与非公平
|
|
|
|
|
**公平锁**是指多个线程按照申请锁的顺序来获取锁,线程直接进入同步队列中排队,队列中最先到的线程先获得锁。**非公平锁**是多个线程加锁时每个线程都会先去尝试获取锁,如果刚好获取到锁,那么线程无需等待,直接执行,如果获取不到锁才会被加入同步队列的队尾等待执行。
|
|
|
|
|
公平锁的优点在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会,而它的缺点相较于于非公平锁整体执行速度更慢,吞吐量更低。同步队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
|
|
|
|
|
而非公平锁非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。它的缺点呢也比较明显,即队列中等待的线程可能一直或者长时间获取不到锁。
|
|
|
|
|
|
|
|
|
|
## 3.2 可重入与非可重入
|
|
|
|
|
可重入锁不会阻塞
|
|
|
|
|
**可重入锁**又名递归锁,是指同一个线程在获取外层同步方法锁的时候,再进入该线程的内层同步方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。**非可重入锁**与可重入锁是对立的关系,即一个线程在获取到外层同步方法锁后,再进入该方法的内层同步方法无法获取到锁,即使锁是同一个对象。
|
|
|
|
|
synchronized与本篇讲的ReentrantLock都属于可重入锁。可重入锁可以有效避免死锁的产生。
|
|
|
|
|
|
|
|
|
|
# 四、CAS与原子类
|
|
|
|
|
|
|
|
|
|
原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如`AtomicBoolean`,`AtomicInteger`,`AtomicLong`。它们分别用于`Boolean`,`Integer`,`Long`类型的原子性操作。它们都是基于CAS实现。
|
|
|
|
|
|
|
|
|
|
## 4.1 CAS机制
|
|
|
|
|
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
|
|
|
|
|
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
|
|
|
|
|
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
|
|
|
|
|
|
|
|
|
|
用伪代码解释就是:
|
|
|
|
|
if(V=A){ ----- 比较相等
|
|
|
|
|
swap() ----- 替换旧值,此时V=B,A=B,新的B需要到下一次修改
|
|
|
|
|
}else {
|
|
|
|
|
retry() ----- 重试(自旋)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
在 Java 中,Java 并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的。Java 代码需通过 JNI 才能调用。CAS 是一条 CPU 的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg。
|
|
|
|
|
![[Snipaste_2023-02-28_13-09-43 5.png]]
|
|
|
|
|
```cpp
|
|
|
|
|
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
|
|
|
|
|
// alternative for InterlockedCompareExchange
|
|
|
|
|
int mp = os::is_MP();
|
|
|
|
|
__asm {
|
|
|
|
|
mov edx,
|
|
|
|
|
dest mov ecx,
|
|
|
|
|
exchange_value mov eax,
|
|
|
|
|
compare_value LOCK_IF_MP(mp)
|
|
|
|
|
cmpxchg dword ptr [edx], ecx
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
|
|
|
|
|
|
|
|
|
|
## 4.2 示例
|
|
|
|
|
1. 在内存地址V中,存储着值为10的变量
|
|
|
|
|
![[5630287-350bc3c474eef0e8.jpg]]
|
|
|
|
|
2. 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
|
|
|
|
|
![[5630287-350bc3c474eef0e8 1.jpg]]
|
|
|
|
|
3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
|
|
|
|
|
![[5630287-350bc3c474eef0e8 2.jpg]]
|
|
|
|
|
4. 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
|
|
|
|
|
![[5630287-350bc3c474eef0e8 3.jpg]]
|
|
|
|
|
5. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
|
|
|
|
|
![[5630287-f638cadea7b6cb96.webp]]
|
|
|
|
|
6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的
|
|
|
|
|
![[5630287-0a3d0b3926366d63.jpg]]
|
|
|
|
|
7. 线程1进行SWAP,把地址V的值替换为B,也就是12。
|
|
|
|
|
![[5630287-0a3d0b3926366d63 1.jpg]]
|
|
|
|
|
|
|
|
|
|
## 4.3 优缺点
|
|
|
|
|
|
|
|
|
|
#### 优点:
|
|
|
|
|
- 可以保证变量操作的原子性;
|
|
|
|
|
- 并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;
|
|
|
|
|
- 在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。
|
|
|
|
|
|
|
|
|
|
#### 缺点:
|
|
|
|
|
1. CPU开销较大
|
|
|
|
|
在并发量比较高的情况下,如果许多线程反复尝试更新(自旋锁
|
|
|
|
|
)某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
|
|
|
|
|
|
|
|
|
|
2. 不能保证代码块的原子性
|
|
|
|
|
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
|
|
|
|
|
|
|
|
|
|
3. ABA问题
|
|
|
|
|
CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareAndSet中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。
|
|
|
|
|
|
|
|
|
|
# 五、AQS
|
|
|
|
|
CAS中的自旋锁有2个缺点:
|
|
|
|
|
+ 第一个是锁饥饿问题。在锁竞争激烈的情况下,可能存在一个线程一直被其他线程”插队“而一直获取不到锁的情况。
|
|
|
|
|
+ 第二是性能问题。在实际的多核上运行的自旋锁在锁竞争激烈时性能较差。
|
|
|
|
|
|
|
|
|
|
TASLock 和 TTASLock 与上文代码类似,都是针对**一个原子状态变量**轮询的自旋锁实现,显然,自旋锁的性能和理想情况相距甚远。这是因为自旋锁锁状态中心化,在竞争激烈的情况下,锁状态变更会导致多个 CPU 的高速缓存的频繁同步,从而拖慢 CPU 效率。
|
|
|
|
|
|
|
|
|
|
为了解决这2个问题就有了CLH锁,CLH是AQS的核心。
|
|
|
|
|
![[721070-20170504110246211-10684485.png]]
|
|
|
|
|
CLH 锁是对自旋锁的一种改进,有效的解决了以上的两个缺点。首先它将线程组织成一个队列,保证先请求的线程先获得锁,避免了饥饿问题。其次锁状态去中心化,**让每个线程在不同的状态变量中自旋**,这样当一个线程释放它的锁时,只能使其后续线程的高速缓存失效,缩小了影响范围,从而减少了 CPU 的开销。
|
|
|
|
|
|
|
|
|
|
CLH 锁数据结构很简单,类似一个链表队列,所有请求获取锁的线程会排列在链表队列中,自旋访问队列中前一个节点的状态。当一个节点释放锁时,只有它的后一个节点才可以得到锁。CLH 锁本身有一个队尾指针 Tail,它是一个原子变量,指向队列最末端的 CLH 节点。每一个 CLH 节点有两个属性:所代表的线程和标识是否持有锁的状态变量。当一个线程要获取锁时,它会对 Tail 进行一个 getAndSet 的原子操作。该操作会返回 Tail 当前指向的节点,也就是当前队尾节点,然后使 Tail 指向这个线程对应的 CLH 节点,成为新的队尾节点。入队成功后,该线程会轮询上一个队尾节点的状态变量,当上一个节点释放锁后,它将得到这个锁。
|
|
|
|
|
|
|
|
|
|
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
|
|
|
|
|
|
|
|
|
|
不同的自定义同步器争用共享资源的方式也不同。**自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可**,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
|
|
|
|
|
|
|
|
|
|
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
|
|
|
|
|
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
|
|
|
|
|
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
|
|
|
|
|
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
|
|
|
|
|
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
|
|
|
|
|
|
|
|
|
|
以ReentrantLock为例,state(同步状态)初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
|
|
|
|
|
|
|
|
|
|
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
|
|
|
|
|
|
|
|
|
|
## 5.1 CLH的数据结构
|
|
|
|
|
CLH中的节点表示待获取锁的对象,里面包含6个方法及属性
|
|
|
|
|
| 方法和属性值 | 含义 |
|
|
|
|
|
| ------------ | -------------------------------------------------------------------------------------------- |
|
|
|
|
|
| waitStatus | 当前节点在队列中的状态 |
|
|
|
|
|
| thread | 表示处于该节点的线程 |
|
|
|
|
|
| prev | 前驱指针 |
|
|
|
|
|
| predecessor | 返回前驱节点,没有的话抛出npe |
|
|
|
|
|
| nextWaiter | 指向下一个处于CONDITION状态的节点(由于本篇文章不讲述Condition Queue队列,这个指针不多介绍) |
|
|
|
|
|
| next | 后继指针 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
waitStatus有下面几个枚举值:
|
|
|
|
|
| 枚举 | 含义 |
|
|
|
|
|
| --------- | ---------------------------------------------- |
|
|
|
|
|
| 0 | 当一个Node被初始化的时候的默认值 |
|
|
|
|
|
| CANCELLED | 为1,表示线程获取锁的请求已经取消了 |
|
|
|
|
|
| CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 |
|
|
|
|
|
| PROPAGATE | 为-3,当前线程处在SHARED情况下,该字段才会使用 |
|
|
|
|
|
| SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 5.2 同步状态State
|
|
|
|
|
AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情况。
|
|
|
|
|
| 方法名 | 描述 |
|
|
|
|
|
| ------------------------------------------------------------------ | -------------------- |
|
|
|
|
|
| protected final int getState() | 获取State的值 |
|
|
|
|
|
| protected final void setState(int newState) | 设置State的值 |
|
|
|
|
|
| protected final boolean compareAndSetState(int expect, int update) | 使用CAS方式更新State |
|
|
|
|
|
|
|
|
|
|
可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)。
|
|
|
|
|
|
|
|
|
|
### 5.2.1 独占模式
|
|
|
|
|
![[27605d483e8935da683a93be015713f331378.png]]
|
|
|
|
|
以ReentrantLock为例,state(同步状态)初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
|
|
|
|
|
|
|
|
|
|
举个例子:排队打饭,只有一个窗口(state),第一个站住窗口(lock(),tryAcquire(),state+1),后一个人需要等待前人离开(unlock,state=0)
|
|
|
|
|
|
|
|
|
|
#### 线程加入等待队列
|
|
|
|
|
当其他线程tryAcquire()失败时,会调用addWaiter加入到等待队列中去。总的来说,一个线程获取锁失败了,被放入等待队列,acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2.2 共享模式
|
|
|
|
|
![[27605d483e8935da683a93be015713f331378 1.png]]
|
|
|
|
|
以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
|