文章目录
1. 悲观锁(阻塞)
1.1. 临界区与竞态条件
1.1.1. 临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块就称为临界区(Critical Section)。易发生指令交错,就会出现前面的问题。
private static int count = 0; // 共享资源
private static void increment()
// 临界区(整个代码块)
{ count++; }
private static void decrement()
// 临界区(整个代码块)
{ count--; }
1.1.2. 竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件(Race Condition)。
⭐ 避免竞态条件的解决方案:
- 阻塞式:synchronized,lock。
- 非阻塞式:原子变量。
1.1.3. 原子性
public class ThreadTest {
private static int count = 0;
public static void main(String[] args) {
// 线程1对count自增5000次
Thread thread1 = new Thread(() -> {
// 临界区,发生了竞态条件
for (int i = 0; i < 5000; i++) count++;
});
// 线程2对count自减5000次
Thread thread2 = new Thread(() -> {
// 临界区,发生了竞态条件
for (int i = 0; i < 5000; i++) count--;
});
thread1.start();
thread2.start();
}
}
- 理想情况下,两个线程运行结束后
count == 0
。 - 实际情况下,两个线程运行结束后
count != 0
。
i++
和 i--
在 java 中不是原子操作。对于 i++
而言(i
为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i--
也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
如果在执行指令的同时,发生了上下文切换,则可能一次自增和自减后 i!=0
。
1.2. synchronized 概念
用来给某个目标(对象,方法等)加锁,相当于不管哪一个线程运行到这个行时,都必须先检查有没有其它线程正在用这个目标,如果有就要等待正在使用的线程运行完后释放该锁,没有的话则对该目标先加锁后再运行。
public class ThreadTest {
private static int count = 0;
// 锁对象
private static Object lock = new Object;
public static void main(String[] args) {
// 线程1对count自增5000次
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) count++;
}
});
// 线程2对count自减5000次
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) count--;
}
});
thread1.start();
thread2.start();
}
}
对关键操作加上 synchronized
后结果就会正确 count = 0
。
⭐️ 对 synchronized 的理解: synchronized
实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,也不会被线程切换所打断。
1.2.1. synchronized 修饰方法
【修饰成员方法】
class Test {
public synchronized void test() {
// 临界区
}
}
// 两者在效果上等价
class Test {
public void test() {
// 对this加锁,相当于把该类对象给锁住(Test对象)
synchronized (this) {
// 临界区
}
}
}
由于是对本类对象加锁,因此当这个类有多个 synchronized
方法时,则多线程调用同一个对象的不同同步方法,会产生锁竞争。
class Test {
public synchronized void test1() {
System.out.println("test1");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void test2() {
System.out.println("test2");
}
}
// 调用代码
Test test = new Test();
// 效果:立即输出"test1"。执行test1(),会对test对象加锁,5s后释放锁
new Thread(() -> test.test1()).start();
// 效果:5s后输出"test2"。线程同步阻塞,等待test对象释放锁后才能执行
new Thread(() -> test.test2()).start();
// 效果:立即输出"test2"。新建的test对象还没有被加锁,可以立即执行
new Thread(() -> new Test().test2()).start();
【修饰静态方法】
class Test {
public synchronized static void test() {
// 临界区
}
}
// 两者在效果上等价
class Test {
public static void test() {
// 静态方法,没有实例对象,只能对类对象加锁(Test.class)
synchronized (Test.class) {
// 临界区
}
}
}
由于是对 class 类对象加锁,因此当这个类有多个 synchronized static
方法时,则多线程调用会产生锁竞争。
class Test {
public synchronized static void test1() {
System.out.println("test1");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized static void test2() {
System.out.println("test2");
}
}
// 调用代码
// 效果:立即输出"test1"。执行test1(),会对Test.class加锁,5s后释放锁
new Thread(() -> Test.test1()).start();
// 效果:5s后输出"test2"。线程同步阻塞,等待Test.class释放锁后才能执行
new Thread(() -> Test.test2()).start();
1.2.2. 变量的线程安全分析
【成员变量和静态变量】
-
如果没共享,则线程安全。
-
如果被共享,根据是否读写来判断:
- 如果只有读操作,则线程安全。
- 如果有读写操作,则这段代码是临界区,线程不安全。
【局部变量】
- 局部变量一般线程安全。
- 局部变量引用的对象,根据是否有方法逃逸来判断:
- 如果该对象没有逃离方法的作用访问,则线程安全。
- 如果该对象逃离方法的作用范围,则线程不安全。
1.2.3. 常见的线程安全类
String
、Integer
、StringBuffer
、Random
、Vecator
、HashTable
、java.util.concurrent
包下的类。
⭐️ 注意:
- 这里的线程安全是指多个线程调用它们同一个实例的某个方法时,是线程安全的。
private static Hashtable<String, Integer> hashtable = new Hashtable<>();
// 多个线程调用test()方法
public static void test() {
// hashtable.put()是原子的,线程安全的
hashtable.put("TEST", 200);
}
- 它们的每个方法是原子的,但它们多个方法的组合不是原子的(可能执行完某一句,但还没执行下一句时,就发生上下文切换)。
private static Hashtable<String, Integer> hashtable = new Hashtable<>();
// 多个线程调用test()方法
public static void test() {
// hashtable.get()是原子的,线程安全的
if (hashtable.get("TEST") == null) {
// hashtable.put()是原子的,线程安全的
hashtable.put("TEST", 200);
}
}
- 类似
String
这种,属于不可变类,自带线程安全属性。
1.3. Monitor(管程)
1.3.1. Java 对象头
由于 Java 面向对象的思想,在 JVM 中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。
【对象头形式】(以32位虚拟机为例)
- 普通对象:
|--------------------------------------------------------|
| Object Header (64 bits) |
|---------------------------|----------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|---------------------------|----------------------------|
- 数组对象:
|---------------------------------------------------------------------------------------|
| Object Header (96 bits) |
|---------------------------|----------------------------|------------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) | array length (32 bits) |
|---------------------------|----------------------------|------------------------------|
【对象头的组成】
主要用来存储对象自身的运行时数据,如hashcode、gc 分代年龄等。mark word
的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word
为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
|-----------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-----------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2(01) | Normal | 无锁
|-----------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2(01) | Biased | 偏向锁
|-----------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2(00) | Lightweight Locked | 轻量级锁
|-----------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2(10) | Heavyweight Locked | 重量级锁
|-----------------------------------------------------------|--------------------|
| | lock:2(11) | Marked for GC | GC标记
|-----------------------------------------------------------|--------------------|
-
lock: 锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
-
identity_hashcode: 对象标识哈希码,采用延迟加载技术。调用方法
System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程 Monitor 中。 -
age: Java 对象的 GC 年龄,最大15。
-
biased_lock: 对象是否启用偏向锁标记。1表示对象启用偏向锁,0表示对象没有偏向锁。
-
thread: 持有偏向锁的线程ID。
-
epoch: 偏向时间戳。
-
ptr_to_lock_record: 指向栈中锁记录的指针。
-
ptr_to_heavyweight_monitor: 指向管程 Monitor 的指针。
1.3.2. Monitor
Monitor 被翻译为监视器或管程。管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized
给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。
【Monitor 的组成和运行】
- 刚开始 Monitor 中 Owner 为
null
。 - 当 Thread-2 执行
synchronized(obj)
时,obj
的对象头中lock
状态会变为重量级锁,并且对象头中ptr_to_heavyweight_monitor
指针会指向该 Monitor 对象,同时会将 Monitor 的所有者 Owner 置为 Thread-2 ,Monitor 同一时间只能有一个 Owner。 - 在 Thread-2 上锁后,如果 Thread-3、Thread-4、Thread-5 也来执行
synchronized(obj)
,由于 Monitor 中的 Owner 不为空,就会进入 EntryList 等待锁被释放并变为 BLOCKED 状态。 - 当 Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来非公平竞争锁。
- WaitSet 中的 Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析。
注意:
synchronized
必须是进入同一个对象的 monitor 才有上述的效果。- 不加
synchronized
的对象不会关联监视器,不遵从以上规则。
1.4. synchronized 原理
锁的状态总共有四种:无锁、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是只升不降)。
1.4.1. 轻量级锁
使用场景: 如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(即没有竞争),那么可以使用轻量级锁来优化。
private static final Object obj = new Object();
public static void method1(){
synchronized (obj){ // ①
method2();
} // ④
}
private static void method2() {
synchronized (obj){ // ②
} // ③
}
【加锁和解锁流程】 默认是主线程调用 method1()
方法
- method1() 尝试对锁对象加锁:
- 虚拟机在当前线程(Main Thread)的栈帧中建立一个锁记录(Lock Record)的空间,包含
锁记录自身的地址
和指向锁对象的指针
。
![](https://i-blog.csdnimg.cn/blog_migrate/4922d5cc9de57cc6984a6ccf4ba65284.png)
- 尝试用 CAS 把
锁记录自身的地址
和锁对象的Mark Word
进行交换,由于锁对象的状态为无锁(01),因此交换成功后状态将变为轻量级锁(00)。
![](https://i-blog.csdnimg.cn/blog_migrate/104822c1160a3496d33ba28fb4c0398e.png)
-
method2() 尝试对锁对象加锁: 但由于锁对象的状态为为轻量级锁(00)且锁对象的 Mark Word 指向当前线程(Main Thread)。因此执行锁重入,则新增一条锁记录作为重入的计数。
-
method2() 尝试对锁对象解锁: 锁记录的地址为
NULL
,表示有重入,只需要移除锁记录,从而使重入计数减1。 -
method1() 尝试对锁对象解锁: 锁记录的地址不为
NULL
,尝试用 CAS 把锁对象的Mark Word
恢复为对象头。因此交换成功后状态将变为无锁(01)。
1.4.2. 重量级锁
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,如果有其它线程为此对象加上了轻量级锁(有竞争),就要进行锁膨胀,将轻量级锁变为重量级锁。
private static final Object obj = new Object();
public static void method(){
synchronized (obj){ // ①
} // ②
}
【加锁和解锁流程】 主线程和其他线程都调用 method()
方法
- 其他线程(Other Thread)尝试对锁对象加轻量级锁: 由于主线程(Main Thread)已经对该对象加了轻量级锁,因此会加锁失败。
![](https://i-blog.csdnimg.cn/blog_migrate/e5a0c8d6f4bc3f9a66f944dc8df41769.png)
于是进入锁膨胀流程:
- 为锁对象申请 Monitor 锁。
- 将锁对象的 Mark Word 指向当前 Monitor ,并把状态置为重量级锁(10)。
- Monitor 的 Owner 则指向
- 其他线程进入 Monitor 的 EntryList 中并变为 BLOCKED 状态。
![](https://i-blog.csdnimg.cn/blog_migrate/cf1391e099797ef956bb534a34d83064.png)
- 主线程(Main Thread)尝试对锁对象解轻量级锁: 尝试用 CAS 把
锁对象的Mark Word
恢复为对象头,失败。于是进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为null
,唤醒 EntryList 中 BLOCKED 线程,并把主线程的锁记录转移至其他线程。
![](https://i-blog.csdnimg.cn/blog_migrate/25cbee7236c45593074a8030be6e2057.png)
1.4.3. 偏向锁
轻量级锁在没有竞争时(只有一个线程),每次重入仍然需要执行CAS操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
【对象创建时】
- 如果启用偏向锁(默认开启):对象创建后,Mark Word 值为0x05(即最后3位为101),且 thread、epoch、age 都为0。
public static void main(String[] args){
Object obj = new Object(); // 默认添加了偏向锁,但是thread为0,不关联任何线程
// obj.hashCode(); // 如果执行,则偏向锁会被去掉,Mark Word后3位为001,hashcode有值,等效于禁用偏向锁
synchronized (obj){ // 保持偏向锁,thread有值,关联当前线程
} // 保持偏向锁,thread有值,关联当前线程(始终偏向当前线程)
}
- 如果禁用偏向锁:对象创建后,Mark Word 值为0x01(即最后3位为001),且 hashcode(第一次使用时才赋值)、age 都为0。
public static void main(String[] args){
Object obj = new Object(); // 无锁状态
synchronized (obj){ // 添加轻量级锁
} // 无锁状态(释放掉轻量级锁)
}
【偏向锁的撤销】
- hashCode(): 调用了对象的
hashCode()
,但偏向锁的对象 MarkWord 中存储的是线程id,如果调用hashCode()
会导致偏向锁被撤销:- 轻量级锁会在锁记录中记录 hashCode。
- 重量级锁会在 Monitor 中记录 hashCode。
- 其他线程抢占: 当有其它线程使用偏向锁对象时,会将偏向锁升级:
- 如果持锁线程未执行完同步代码块:偏向锁 --> 重量级锁。
- 如果持锁线程已执行完同步代码块:偏向锁 --> 轻量级锁。
- wait()/notify(): 调用了对象的
wait()
或notify()
方法时,由于只有重量级锁才有效,所以偏向锁 --> 重量级锁。
【批量重偏向与批量撤销】
- 批量重偏向: 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重
置对象的Thread ID。当撤销偏向锁阈值超过20次后,jvm会在给这些对象加锁时重新偏向至加锁线程。 - 批量撤销: 当撤销偏向锁阈值超过40次后,jvm 会把整个类的所有对象都变为不可偏向的,新建的对象也是不可偏向的。
1.4.4. 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(从而避免上下文切换)。如果自旋超时,则会自旋失败,当前线程就会进入阻塞状态。
⭐️ 注意:
-
自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
-
Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。
-
Java 7之后不能控制是否开启自旋功能。
1.4.5. 同步消除
如果能确定一个对象不会出现线程逃逸,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。(即锁和锁块内的对象不会逃逸出线程,就可以把这个同步块取消)
public static void alloc() {
byte[] b = new byte[2];
// 不会线程逃逸,所以该同步锁可以去掉
// 开启使用同步消除执行时间 10 ms左右
// 关闭使用同步消除执行时间 3870 ms左右
synchronized (b) {
b[0] = 1;
}
}
public static void main(String[] args) {
for (int i = 0; i < 100000000; i++) {
alloc();
}
}
1.5. wait() 和 notify()
1.5.1. 原理
Owner 线程发现条件不满足,调用 wait()
方法,即可进入 WaitSet 变为 WAITING 状态。
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片。
- BLOCKED 线程会在 Owner 线程释放锁时唤醒;WAITING 线程会在 Owner 线程调用
notify()
或notifyAll()
时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争。
1.5.2. sleep() 和 wait() 的区别
sleep()
是Thread
方法,而wait()
是Object
的方法。sleep()
不需要强制和synchronized
配合使用,但wait()
需要和synchronized
一起用。sleep()
在睡眠的同时不会释放对象锁的,但wait()
在等待的时候会释放对象锁。sleep()
线程的状态是 TIMED_WAITING ,wait()
不设置时间是 WAITING,设置了时间是 TIMED_WAITING。
1.5.3. wait() 和 notify() 的正确使用
synchronized(lock){
while(工作/唤醒条件不成立){
lock.wait();
}
// TODO:执行工作任务
}
// 其他线程
synchronized(lock){
// TODO:修改工作/唤醒条件
// 唤醒全部,不满足条件的再继续wait()
lock.notifyAll();
}
1.5.4. 同步模式-保护性暂停
一个线程等待另一个线程的结果。 和 join()
的区别:join()
是一个线程等待另一个线程运行结束。
![](https://i-blog.csdnimg.cn/blog_migrate/9b21169b6eceab16f3e03e90b8cc3805.png)
public class GuardedObject<T> {
private T data;
// 获取数据
public T get() {
synchronized (this) {
while (data == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return data;
}
}
// 获取数据(超时)
public T get(long timeout) {
synchronized (this) {
// 开始等待的时间
long beginTime = System.currentTimeMillis();
// 已经等待的时间
long passedTime = 0;
while (data == null) {
// 还需要等待的时间
long waitTime = timeout - passedTime;
// 如果已经超时,则退出循环
if (waitTime < 0) break;
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 计算已经等待了多久
passedTime = System.currentTimeMillis() - beginTime;
}
return data;
}
}
// 设置计算结果
public void complete(T data) {
synchronized (this){
this.data = data;
this.notifyAll();
}
}
}
1.5.5. 异步模式-消息队列
- 与保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应。
- 消费队列可以用来平衡生产和消费的线程资源。
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据。
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据。
- JDK中各种阻塞队列,采用的就是这种模式。
public class MessageQueue<T> {
private final LinkedList<T> queue = new LinkedList<>();
private final int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
// 从消息队列获取消息
public T take() {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.notifyAll();
return queue.removeFirst();
}
}
// 向消息队列添加消息
public void put(T data) {
synchronized (queue) {
while (queue.size() >= capacity) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(data);
queue.notifyAll();
}
}
}
1.6. park() 和 unpark()
LockSupport.park()
和 LockSupport.unpark()
实现线程的暂停和继续。阻塞状态为 WAITING。
LockSupport.park(); // 检查有没有通行证,没有则暂停线程 WAITING
LockSupport.unpark(); // 颁发一张通行证,继续运行线程 RUNNABLE
如果先调用 unpark()
再调用 park()
,则线程不会暂停。
LockSupport.unpark(); // 先颁发一张通行证 TIMED_WAITING
LockSupport.park(); // 检查有没有通行证,有则不暂停线程 RUNNABLE
1.7. 线程活跃性
1.7.1. 死锁
一个线程需要同时获取多把锁,这时就容易发生死锁。
t1
线程获得A对象
的锁,接下来想获取B对象
的锁。t2
线程获得B对象
的锁,接下来想获取A对象
的锁。
Thread t1 = new Thread(() -> {
synchronized (A) {
Thread.sleep(1000);
synchronized (B) {
}
}
});
Thread t2 = new Thread(() -> {
synchronized (B) {
Thread.sleep(500);
synchronized (A) {
}
}
});
1.7.2. 活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
两个线程增加随机睡眠时间,可以防止活锁。
int count = 10;
Thread t1 = new Thread(() -> {
// 期望减到0就退出
while (count > 0) {
Thread.sleep(200);
count--;
System.out.println("- " + count);
}
});
Thread t2 = new Thread(() -> {
// 期望加到20就退出
while (count < 20) {
Thread.sleep(200);
count++;
System.out.println(" " + count);
}
});
1.7.3. 饥饿
一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束。
饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。
1.8. ReentrantLock(可重入锁)
可重入是指同一个线程如果获得了这把锁,当再次获取这把锁时不会被锁挡住。
1.8.1. API
ReentrantLock lock = new ReentrantLock();
创建一个可重入锁对象 (非公平锁)。ReentrantLock lock = new ReentrantLock(true);
创建一个可重入锁对象 (公平锁)。lock.lock();
获取可重入锁 (不可被中断),失败则进入阻塞状态直到成功。lock.lockInterruptibly();
获取可重入锁 (可被中断),失败则进入阻塞状态直到成功。lock.tryLock();
尝试获取可重入锁 (可被中断,可设置超时时间),成功立即返回true
,失败立即返回true
。lock.unlock();
释放可重入锁。lock.newCondition();
创建一个条件变量
1.8.2. 使用方法
ReentrantLock reentrantLock = new ReentrantLock();
// 获取锁
reentrantLock.lock();
// 获得成功
try {
// 临界区
} finally {
// 无论如何都要释放锁
reentrantLock.unlock();
}
1.8.3. 条件变量
ReentrantLock reentrantLock = new ReentrantLock();
创建一个可重入锁。Condition condition = reentrantLock.newCondition();
创建一个条件变量 (可以调用多次创建多个条件变量)。condition.await();
将当前线程和condition
关联,并进入 WaitSet 等待。condition.signal();
唤醒一个和condition
关联的线程。condition.signalAll();
唤醒全部和condition
关联的线程。
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
// 条件变量
Condition condition1 = reentrantLock.newCondition();
Condition condition2 = reentrantLock.newCondition();
Thread t1 = new Thread(() -> {
reentrantLock.lock();
try {
// 将t1线程和condition1关联,并进入WaitSet等待
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
});
Thread t2 = new Thread(() -> {
reentrantLock.lock();
try {
// 将t2线程和condition2关联,并进入WaitSet等待
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
reentrantLock.lock();
try {
// 唤醒一个和condition1关联的线程
condition1.signal();
} finally {
reentrantLock.unlock();
}
Thread.sleep(1000);
reentrantLock.lock();
try {
// 唤醒全部和condition2关联的线程
condition2.signalAll();
} finally {
reentrantLock.unlock();
}
}
2. 乐观锁(非阻塞)
每次操作数据的时候,都认为其他线程不会参与竞争修改,所以不加锁。如果操作成功了最好,如果失败也不会阻塞,可以采取一些补偿机制(反复重试)。
2.1. CAS 指令
CAS(Compare-and-Swap 或 Compare-and-Set) 指令是由 CPU 直接保证原子性的。有三个操作数:内存值 V,预期值 A,新值 B,当且仅当 V 符合预期值 A 时,将内存值 V 修改为 B,否则什么也不做。
CAS 体现的是无锁并发、无阻塞并发:
- 没有使用 synchronized,所以线程不会陷入阻塞(减少上下文切换),提升效率。
- 如果竞争激烈,则重试必然频繁发生,反而降低效率。
2.2. 原子类
AtomicBoolean
、AtomicInteger
、AtomicLong
。
2.2.1. 原理
以 AtomicInteger 的 incrementAndGet()
方法实现 ++i
操作为例:
// AtomicInteger.java
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
// Unsafe.java
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
// 循环继续尝试更新,直到 weakCompareAndSetInt 返回true
do {
// 先获取当前的 value 最新值
v = getIntVolatile(o, offset);
} while (
// 进行原子更新操作:
// 先检查当前 value 是否等于 current。
// 如果相等,则返回 true,意味着 value 没被其他线程修改过,并更新为目标值。
// 如果不等,则返回 false。
!weakCompareAndSetInt(o, offset, v, v + delta)
);
return v;
}
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
return compareAndSetInt(o, offset, expected, x);
}
// native 方法:使用CAS机器指令,直接保证原子性
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
2.2.2. ABA 问题
【什么是 ABA 问题】
一个线程在执行 CAS 时仅能判断出共享变量的值与最初值 A 是否相同,却不能感知到这种从 A 改为 B 又改回 A 的情况。
public class ThreadTest {
private static final AtomicReference<String> reference = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
String prev = reference.get();
System.out.println("原始值:" + prev);
// 子线程把A改成B之后又改成A
new Thread(() -> {
System.out.println("子线程 A->B:" + reference.compareAndSet(reference.get(), "B"));
System.out.println("子线程 B->A:" + reference.compareAndSet(reference.get(), "A"));
}).start();
Thread.sleep(1000);
// 主线程没有感知到发生过的变化
System.out.println("主线程 A->C:" + reference.compareAndSet(prev, "C"));
}
}
原始值:A
子线程 A->B:true
子线程 B->A:true
主线程 A->C:true
【计数解决 ABA 问题】
AtomicStampedReference
public class ThreadTest {
private static final AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
String prev = reference.getReference();
int prevStamp = reference.getStamp();
System.out.println("原始值 " + prev);
new Thread(() -> {
System.out.println("子线程 A->B:" + reference.compareAndSet(reference.getReference(), "B", reference.getStamp(), reference.getStamp() + 1));
System.out.println("子线程 B->A:" + reference.compareAndSet(reference.getReference(), "A", reference.getStamp(), reference.getStamp() + 1));
}).start();
Thread.sleep(1000);
System.out.println("主线程 A->C:" + reference.compareAndSet(prev, "C", prevStamp, prevStamp + 1));
}
}
原始值:A
子线程 A->B:true
子线程 B->A:true
主线程 A->C:false
【标记解决 ABA 问题】
public class ThreadTest {
private static final AtomicMarkableReference<String> reference = new AtomicMarkableReference<>("A", false);
public static void main(String[] args) throws InterruptedException {
String prev = reference.getReference();
boolean marked = reference.isMarked();
System.out.println("原始值 " + prev);
new Thread(() -> {
System.out.println("子线程 A->B:" + reference.compareAndSet(reference.getReference(), "B", reference.isMarked(), true));
System.out.println("子线程 B->A:" + reference.compareAndSet(reference.getReference(), "A", reference.isMarked(), true));
}).start();
Thread.sleep(1000);
System.out.println("主线程 A->C:" + reference.compareAndSet(prev, "C", marked, !marked));
}
}
原始值:A
子线程 A->B:true
子线程 B->A:true
主线程 A->C:false