1. 悲观锁(阻塞)

1.1. 临界区与竞态条件

1.1.1. 临界区

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块就称为临界区(Critical Section)。易发生指令交错,就会出现前面的问题。

  1. private static int count = 0; // 共享资源
  2. private static void increment()
  3. // 临界区(整个代码块)
  4. { count++; }
  5. private static void decrement()
  6. // 临界区(整个代码块)
  7. { count--; }

1.1.2. 竞态条件

多个线程临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件(Race Condition)

⭐ 避免竞态条件的解决方案:

  • 阻塞式:synchronized,lock。
  • 非阻塞式:原子变量。

1.1.3. 原子性

  1. public class ThreadTest {
  2. private static int count = 0;
  3. public static void main(String[] args) {
  4. // 线程1对count自增5000次
  5. Thread thread1 = new Thread(() -> {
  6. // 临界区,发生了竞态条件
  7. for (int i = 0; i < 5000; i++) count++;
  8. });
  9. // 线程2对count自减5000次
  10. Thread thread2 = new Thread(() -> {
  11. // 临界区,发生了竞态条件
  12. for (int i = 0; i < 5000; i++) count--;
  13. });
  14. thread1.start();
  15. thread2.start();
  16. }
  17. }
  • 理想情况下,两个线程运行结束后 count == 0
  • 实际情况下,两个线程运行结束后 count != 0

i++i-- 在 java 中不是原子操作。对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 准备常量1
  3. iadd // 自增
  4. putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 准备常量1
  3. isub // 自减
  4. putstatic i // 将修改后的值存入静态变量i

如果在执行指令的同时,发生了上下文切换,则可能一次自增和自减后 i!=0

synchronized原理(悲观锁和乐观锁) - 图1

1.2. synchronized 概念

用来给某个目标(对象,方法等)加锁,相当于不管哪一个线程运行到这个行时,都必须先检查有没有其它线程正在用这个目标,如果有就要等待正在使用的线程运行完后释放该锁,没有的话则对该目标先加锁再运行

  1. public class ThreadTest {
  2. private static int count = 0;
  3. // 锁对象
  4. private static Object lock = new Object;
  5. public static void main(String[] args) {
  6. // 线程1对count自增5000次
  7. Thread thread1 = new Thread(() -> {
  8. for (int i = 0; i < 5000; i++) {
  9. synchronized (lock) count++;
  10. }
  11. });
  12. // 线程2对count自减5000次
  13. Thread thread2 = new Thread(() -> {
  14. for (int i = 0; i < 5000; i++) {
  15. synchronized (lock) count--;
  16. }
  17. });
  18. thread1.start();
  19. thread2.start();
  20. }
  21. }

对关键操作加上 synchronized 后结果就会正确 count = 0

synchronized原理(悲观锁和乐观锁) - 图2

⭐️ 对 synchronized 的理解: synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,也不会被线程切换所打断。

1.2.1. synchronized 修饰方法

【修饰成员方法】

  1. class Test {
  2. public synchronized void test() {
  3. // 临界区
  4. }
  5. }
  6. // 两者在效果上等价
  7. class Test {
  8. public void test() {
  9. // 对this加锁,相当于把该类对象给锁住(Test对象)
  10. synchronized (this) {
  11. // 临界区
  12. }
  13. }
  14. }

由于是对本类对象加锁,因此当这个类有多个 synchronized 方法时,则多线程调用同一个对象的不同同步方法,会产生锁竞争。

  1. class Test {
  2. public synchronized void test1() {
  3. System.out.println("test1");
  4. try {
  5. Thread.sleep(5000);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. public synchronized void test2() {
  11. System.out.println("test2");
  12. }
  13. }
  14. // 调用代码
  15. Test test = new Test();
  16. // 效果:立即输出"test1"。执行test1(),会对test对象加锁,5s后释放锁
  17. new Thread(() -> test.test1()).start();
  18. // 效果:5s后输出"test2"。线程同步阻塞,等待test对象释放锁后才能执行
  19. new Thread(() -> test.test2()).start();
  20. // 效果:立即输出"test2"。新建的test对象还没有被加锁,可以立即执行
  21. new Thread(() -> new Test().test2()).start();

【修饰静态方法】

  1. class Test {
  2. public synchronized static void test() {
  3. // 临界区
  4. }
  5. }
  6. // 两者在效果上等价
  7. class Test {
  8. public static void test() {
  9. // 静态方法,没有实例对象,只能对类对象加锁(Test.class)
  10. synchronized (Test.class) {
  11. // 临界区
  12. }
  13. }
  14. }

由于是对 class 类对象加锁,因此当这个类有多个 synchronized static 方法时,则多线程调用会产生锁竞争。

  1. class Test {
  2. public synchronized static void test1() {
  3. System.out.println("test1");
  4. try {
  5. Thread.sleep(5000);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. public synchronized static void test2() {
  11. System.out.println("test2");
  12. }
  13. }
  14. // 调用代码
  15. // 效果:立即输出"test1"。执行test1(),会对Test.class加锁,5s后释放锁
  16. new Thread(() -> Test.test1()).start();
  17. // 效果:5s后输出"test2"。线程同步阻塞,等待Test.class释放锁后才能执行
  18. new Thread(() -> Test.test2()).start();

1.2.2. 变量的线程安全分析

【成员变量和静态变量】

  • 如果没共享,则线程安全
  • 如果被共享,根据是否读写来判断:
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,线程不安全。

【局部变量】

  • 局部变量一般线程安全
  • 局部变量引用的对象,根据是否有方法逃逸来判断:
    • 如果该对象没有逃离方法的作用访问,则线程安全
    • 如果该对象逃离方法的作用范围,则线程不安全

1.2.3. 常见的线程安全类

StringIntegerStringBufferRandomVecatorHashTablejava.util.concurrent 包下的类。

⭐️ 注意:

  • 这里的线程安全是指多个线程调用它们同一个实例的某个方法时,是线程安全的。
  1. private static Hashtable<String, Integer> hashtable = new Hashtable<>();
  2. // 多个线程调用test()方法
  3. public static void test() {
  4. // hashtable.put()是原子的,线程安全的
  5. hashtable.put("TEST", 200);
  6. }
  • 它们的每个方法是原子的,但它们多个方法的组合不是原子的(可能执行完某一句,但还没执行下一句时,就发生上下文切换)。
  1. private static Hashtable<String, Integer> hashtable = new Hashtable<>();
  2. // 多个线程调用test()方法
  3. public static void test() {
  4. // hashtable.get()是原子的,线程安全的
  5. if (hashtable.get("TEST") == null) {
  6. // hashtable.put()是原子的,线程安全的
  7. hashtable.put("TEST", 200);
  8. }
  9. }
  • 类似 String 这种,属于不可变类,自带线程安全属性。

1.3. Monitor(管程)

1.3.1. Java 对象头

由于 Java 面向对象的思想,在 JVM 中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

【对象头形式】(以32位虚拟机为例)

  • 普通对象:
  1. |--------------------------------------------------------|
  2. | Object Header (64 bits) |
  3. |---------------------------|----------------------------|
  4. | Mark Word (32 bits) | Klass Word (32 bits) |
  5. |---------------------------|----------------------------|
  • 数组对象:
  1. |---------------------------------------------------------------------------------------|
  2. | Object Header (96 bits) |
  3. |---------------------------|----------------------------|------------------------------|
  4. | Mark Word (32 bits) | Klass Word (32 bits) | array length (32 bits) |
  5. |---------------------------|----------------------------|------------------------------|

【对象头的组成】

主要用来存储对象自身的运行时数据,如hashcode、gc 分代年龄等。mark word 的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

  1. |-----------------------------------------------------------|--------------------|
  2. | Mark Word (32 bits) | State |
  3. |-----------------------------------------------------------|--------------------|
  4. | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2(01) | Normal | 无锁
  5. |-----------------------------------------------------------|--------------------|
  6. | thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2(01) | Biased | 偏向锁
  7. |-----------------------------------------------------------|--------------------|
  8. | ptr_to_lock_record:30 | lock:2(00) | Lightweight Locked | 轻量级锁
  9. |-----------------------------------------------------------|--------------------|
  10. | ptr_to_heavyweight_monitor:30 | lock:2(10) | Heavyweight Locked | 重量级锁
  11. |-----------------------------------------------------------|--------------------|
  12. | | lock:2(11) | Marked for GC | GC标记
  13. |-----------------------------------------------------------|--------------------|
  • 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 的组成和运行】

synchronized原理(悲观锁和乐观锁) - 图3

  • 刚开始 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. 轻量级锁

使用场景: 如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(即没有竞争),那么可以使用轻量级锁来优化。

  1. private static final Object obj = new Object();
  2. public static void method1(){
  3. synchronized (obj){ // ①
  4. method2();
  5. } // ④
  6. }
  7. private static void method2() {
  8. synchronized (obj){ // ②
  9. } // ③
  10. }

【加锁和解锁流程】 默认是主线程调用 method1() 方法

  1. method1() 尝试对锁对象加锁:
  • 虚拟机在当前线程(Main Thread)的栈帧中建立一个锁记录(Lock Record)的空间,包含锁记录自身的地址指向锁对象的指针

synchronized原理(悲观锁和乐观锁) - 图4

  • 尝试用 CAS 把锁记录自身的地址锁对象的Mark Word进行交换,由于锁对象的状态为无锁(01),因此交换成功后状态将变为轻量级锁(00)。

synchronized原理(悲观锁和乐观锁) - 图5

  1. method2() 尝试对锁对象加锁: 但由于锁对象的状态为为轻量级锁(00)且锁对象的 Mark Word 指向当前线程(Main Thread)。因此执行锁重入,则新增一条锁记录作为重入的计数。 synchronized原理(悲观锁和乐观锁) - 图6
  2. method2() 尝试对锁对象解锁: 锁记录的地址为 NULL ,表示有重入,只需要移除锁记录,从而使重入计数减1。 synchronized原理(悲观锁和乐观锁) - 图7
  3. method1() 尝试对锁对象解锁: 锁记录的地址不为 NULL ,尝试用 CAS 把锁对象的Mark Word恢复为对象头。因此交换成功后状态将变为无锁(01)。

1.4.2. 重量级锁

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,如果有其它线程为此对象加上了轻量级锁(有竞争),就要进行锁膨胀,将轻量级锁变为重量级锁

  1. private static final Object obj = new Object();
  2. public static void method(){
  3. synchronized (obj){ // ①
  4. } // ②
  5. }

【加锁和解锁流程】 主线程和其他线程都调用 method() 方法

  1. 其他线程(Other Thread)尝试对锁对象加轻量级锁: 由于主线程(Main Thread)已经对该对象加了轻量级锁,因此会加锁失败。

synchronized原理(悲观锁和乐观锁) - 图8

于是进入锁膨胀流程:

  • 为锁对象申请 Monitor 锁。
  • 将锁对象的 Mark Word 指向当前 Monitor ,并把状态置为重量级锁(10)。
  • Monitor 的 Owner 则指向
  • 其他线程进入 Monitor 的 EntryList 中并变为 BLOCKED 状态。

synchronized原理(悲观锁和乐观锁) - 图9

  1. 主线程(Main Thread)尝试对锁对象解轻量级锁: 尝试用 CAS 把锁对象的Mark Word恢复为对象头,失败。于是进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程,并把主线程的锁记录转移至其他线程。

synchronized原理(悲观锁和乐观锁) - 图10

1.4.3. 偏向锁

轻量级锁在没有竞争时(只有一个线程),每次重入仍然需要执行CAS操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

【对象创建时】

  • 如果启用偏向锁(默认开启):对象创建后,Mark Word 值为0x05(即最后3位为101),且 thread、epoch、age 都为0。
  1. public static void main(String[] args){
  2. Object obj = new Object(); // 默认添加了偏向锁,但是thread为0,不关联任何线程
  3. // obj.hashCode(); // 如果执行,则偏向锁会被去掉,Mark Word后3位为001,hashcode有值,等效于禁用偏向锁
  4. synchronized (obj){ // 保持偏向锁,thread有值,关联当前线程
  5. } // 保持偏向锁,thread有值,关联当前线程(始终偏向当前线程)
  6. }
  • 如果禁用偏向锁:对象创建后,Mark Word 值为0x01(即最后3位为001),且 hashcode(第一次使用时才赋值)、age 都为0。
  1. public static void main(String[] args){
  2. Object obj = new Object(); // 无锁状态
  3. synchronized (obj){ // 添加轻量级锁
  4. } // 无锁状态(释放掉轻量级锁)
  5. }

【偏向锁的撤销】

  • hashCode(): 调用了对象的 hashCode(),但偏向锁的对象 MarkWord 中存储的是线程id,如果调用 hashCode() 会导致偏向锁被撤销:
    • 轻量级锁会在锁记录中记录 hashCode。
    • 重量级锁会在 Monitor 中记录 hashCode。
  • 其他线程抢占: 当有其它线程使用偏向锁对象时,会将偏向锁升级:
    • 如果持锁线程未执行完同步代码块:偏向锁 —> 重量级锁。
    • 如果持锁线程已执行完同步代码块:偏向锁 —> 轻量级锁。
  • wait()/notify(): 调用了对象的 wait()notify()方法时,由于只有重量级锁才有效,所以偏向锁 —> 重量级锁。

【批量重偏向与批量撤销】

  • 批量重偏向: 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重
    置对象的Thread ID。当撤销偏向锁阈值超过20次后,jvm会在给这些对象加锁时重新偏向至加锁线程。
  • 批量撤销: 当撤销偏向锁阈值超过40次后,jvm 会把整个类的所有对象都变为不可偏向的,新建的对象也是不可偏向的。

1.4.4. 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(从而避免上下文切换)。如果自旋超时,则会自旋失败,当前线程就会进入阻塞状态。

synchronized原理(悲观锁和乐观锁) - 图11

⭐️ 注意:

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。
  • Java 7之后不能控制是否开启自旋功能。

1.4.5. 同步消除

如果能确定一个对象不会出现线程逃逸,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。(即锁和锁块内的对象不会逃逸出线程,就可以把这个同步块取消)

  1. public static void alloc() {
  2. byte[] b = new byte[2];
  3. // 不会线程逃逸,所以该同步锁可以去掉
  4. // 开启使用同步消除执行时间 10 ms左右
  5. // 关闭使用同步消除执行时间 3870 ms左右
  6. synchronized (b) {
  7. b[0] = 1;
  8. }
  9. }
  10. public static void main(String[] args) {
  11. for (int i = 0; i < 100000000; i++) {
  12. alloc();
  13. }
  14. }

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() 的正确使用

  1. synchronized(lock){
  2. while(工作/唤醒条件不成立){
  3. lock.wait();
  4. }
  5. // TODO:执行工作任务
  6. }
  7. // 其他线程
  8. synchronized(lock){
  9. // TODO:修改工作/唤醒条件
  10. // 唤醒全部,不满足条件的再继续wait()
  11. lock.notifyAll();
  12. }

1.5.4. 同步模式-保护性暂停

一个线程等待另一个线程的结果。join() 的区别:join() 是一个线程等待另一个线程运行结束。

synchronized原理(悲观锁和乐观锁) - 图12

  1. public class GuardedObject<T> {
  2. private T data;
  3. // 获取数据
  4. public T get() {
  5. synchronized (this) {
  6. while (data == null) {
  7. try {
  8. this.wait();
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. return data;
  14. }
  15. }
  16. // 获取数据(超时)
  17. public T get(long timeout) {
  18. synchronized (this) {
  19. // 开始等待的时间
  20. long beginTime = System.currentTimeMillis();
  21. // 已经等待的时间
  22. long passedTime = 0;
  23. while (data == null) {
  24. // 还需要等待的时间
  25. long waitTime = timeout - passedTime;
  26. // 如果已经超时,则退出循环
  27. if (waitTime < 0) break;
  28. try {
  29. this.wait(waitTime);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. // 计算已经等待了多久
  34. passedTime = System.currentTimeMillis() - beginTime;
  35. }
  36. return data;
  37. }
  38. }
  39. // 设置计算结果
  40. public void complete(T data) {
  41. synchronized (this){
  42. this.data = data;
  43. this.notifyAll();
  44. }
  45. }
  46. }

1.5.5. 异步模式-消息队列

  • 与保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应。
  • 消费队列可以用来平衡生产和消费的线程资源。
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据。
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据。
  • JDK中各种阻塞队列,采用的就是这种模式。
  1. public class MessageQueue<T> {
  2. private final LinkedList<T> queue = new LinkedList<>();
  3. private final int capacity;
  4. public MessageQueue(int capacity) {
  5. this.capacity = capacity;
  6. }
  7. // 从消息队列获取消息
  8. public T take() {
  9. synchronized (queue) {
  10. while (queue.isEmpty()) {
  11. try {
  12. queue.wait();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. queue.notifyAll();
  18. return queue.removeFirst();
  19. }
  20. }
  21. // 向消息队列添加消息
  22. public void put(T data) {
  23. synchronized (queue) {
  24. while (queue.size() >= capacity) {
  25. try {
  26. queue.wait();
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. queue.addLast(data);
  32. queue.notifyAll();
  33. }
  34. }
  35. }

1.6. park() 和 unpark()

LockSupport.park()LockSupport.unpark() 实现线程的暂停和继续。阻塞状态为 WAITING。

  1. LockSupport.park(); // 检查有没有通行证,没有则暂停线程 WAITING
  2. LockSupport.unpark(); // 颁发一张通行证,继续运行线程 RUNNABLE

如果先调用 unpark() 再调用 park() ,则线程不会暂停。

  1. LockSupport.unpark(); // 先颁发一张通行证 TIMED_WAITING
  2. LockSupport.park(); // 检查有没有通行证,有则不暂停线程 RUNNABLE

1.7. 线程活跃性

1.7.1. 死锁

一个线程需要同时获取多把锁,这时就容易发生死锁。

  • t1 线程获得 A对象 的锁,接下来想获取 B对象 的锁。
  • t2 线程获得 B对象 的锁,接下来想获取 A对象 的锁。

synchronized原理(悲观锁和乐观锁) - 图13

  1. Thread t1 = new Thread(() -> {
  2. synchronized (A) {
  3. Thread.sleep(1000);
  4. synchronized (B) {
  5. }
  6. }
  7. });
  8. Thread t2 = new Thread(() -> {
  9. synchronized (B) {
  10. Thread.sleep(500);
  11. synchronized (A) {
  12. }
  13. }
  14. });

1.7.2. 活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。

两个线程增加随机睡眠时间,可以防止活锁。

  1. int count = 10;
  2. Thread t1 = new Thread(() -> {
  3. // 期望减到0就退出
  4. while (count > 0) {
  5. Thread.sleep(200);
  6. count--;
  7. System.out.println("- " + count);
  8. }
  9. });
  10. Thread t2 = new Thread(() -> {
  11. // 期望加到20就退出
  12. while (count < 20) {
  13. Thread.sleep(200);
  14. count++;
  15. System.out.println(" " + count);
  16. }
  17. });

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. 使用方法

  1. ReentrantLock reentrantLock = new ReentrantLock();
  2. // 获取锁
  3. reentrantLock.lock();
  4. // 获得成功
  5. try {
  6. // 临界区
  7. } finally {
  8. // 无论如何都要释放锁
  9. reentrantLock.unlock();
  10. }

1.8.3. 条件变量

  • ReentrantLock reentrantLock = new ReentrantLock(); 创建一个可重入锁。
  • Condition condition = reentrantLock.newCondition(); 创建一个条件变量 (可以调用多次创建多个条件变量)
  • condition.await(); 将当前线程和 condition 关联,并进入 WaitSet 等待。
  • condition.signal(); 唤醒一个和 condition 关联的线程。
  • condition.signalAll(); 唤醒全部和 condition 关联的线程。
  1. public static void main(String[] args) throws InterruptedException {
  2. ReentrantLock reentrantLock = new ReentrantLock();
  3. // 条件变量
  4. Condition condition1 = reentrantLock.newCondition();
  5. Condition condition2 = reentrantLock.newCondition();
  6. Thread t1 = new Thread(() -> {
  7. reentrantLock.lock();
  8. try {
  9. // 将t1线程和condition1关联,并进入WaitSet等待
  10. condition1.await();
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. } finally {
  14. reentrantLock.unlock();
  15. }
  16. });
  17. Thread t2 = new Thread(() -> {
  18. reentrantLock.lock();
  19. try {
  20. // 将t2线程和condition2关联,并进入WaitSet等待
  21. condition2.await();
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. } finally {
  25. reentrantLock.unlock();
  26. }
  27. });
  28. t1.start();
  29. t2.start();
  30. Thread.sleep(1000);
  31. reentrantLock.lock();
  32. try {
  33. // 唤醒一个和condition1关联的线程
  34. condition1.signal();
  35. } finally {
  36. reentrantLock.unlock();
  37. }
  38. Thread.sleep(1000);
  39. reentrantLock.lock();
  40. try {
  41. // 唤醒全部和condition2关联的线程
  42. condition2.signalAll();
  43. } finally {
  44. reentrantLock.unlock();
  45. }
  46. }

2. 乐观锁(非阻塞)

每次操作数据的时候,都认为其他线程不会参与竞争修改,所以不加锁。如果操作成功了最好,如果失败也不会阻塞,可以采取一些补偿机制(反复重试)。

2.1. CAS 指令

CAS(Compare-and-Swap 或 Compare-and-Set) 指令是由 CPU 直接保证原子性的。有三个操作数:内存值 V,预期值 A,新值 B,当且仅当 V 符合预期值 A 时,将内存值 V 修改为 B,否则什么也不做。

CAS 体现的是无锁并发、无阻塞并发:

  • 没有使用 synchronized,所以线程不会陷入阻塞(减少上下文切换),提升效率。
  • 如果竞争激烈,则重试必然频繁发生,反而降低效率。

2.2. 原子类

AtomicBooleanAtomicIntegerAtomicLong

2.2.1. 原理

以 AtomicInteger 的 incrementAndGet() 方法实现 ++i 操作为例:

  1. // AtomicInteger.java
  2. public final int incrementAndGet() {
  3. return U.getAndAddInt(this, VALUE, 1) + 1;
  4. }
  5. // Unsafe.java
  6. @HotSpotIntrinsicCandidate
  7. public final int getAndAddInt(Object o, long offset, int delta) {
  8. int v;
  9. // 循环继续尝试更新,直到 weakCompareAndSetInt 返回true
  10. do {
  11. // 先获取当前的 value 最新值
  12. v = getIntVolatile(o, offset);
  13. } while (
  14. // 进行原子更新操作:
  15. // 先检查当前 value 是否等于 current。
  16. // 如果相等,则返回 true,意味着 value 没被其他线程修改过,并更新为目标值。
  17. // 如果不等,则返回 false。
  18. !weakCompareAndSetInt(o, offset, v, v + delta)
  19. );
  20. return v;
  21. }
  22. @HotSpotIntrinsicCandidate
  23. public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
  24. return compareAndSetInt(o, offset, expected, x);
  25. }
  26. // native 方法:使用CAS机器指令,直接保证原子性
  27. @HotSpotIntrinsicCandidate
  28. public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);

2.2.2. ABA 问题

【什么是 ABA 问题】

一个线程在执行 CAS 时仅能判断出共享变量的值与最初值 A 是否相同,却不能感知到这种从 A 改为 B 又改回 A 的情况。

  1. public class ThreadTest {
  2. private static final AtomicReference<String> reference = new AtomicReference<>("A");
  3. public static void main(String[] args) throws InterruptedException {
  4. String prev = reference.get();
  5. System.out.println("原始值:" + prev);
  6. // 子线程把A改成B之后又改成A
  7. new Thread(() -> {
  8. System.out.println("子线程 A->B:" + reference.compareAndSet(reference.get(), "B"));
  9. System.out.println("子线程 B->A:" + reference.compareAndSet(reference.get(), "A"));
  10. }).start();
  11. Thread.sleep(1000);
  12. // 主线程没有感知到发生过的变化
  13. System.out.println("主线程 A->C:" + reference.compareAndSet(prev, "C"));
  14. }
  15. }

原始值:A

子线程 A->B:true

子线程 B->A:true

主线程 A->C:true

【计数解决 ABA 问题】

AtomicStampedReference

  1. public class ThreadTest {
  2. private static final AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 0);
  3. public static void main(String[] args) throws InterruptedException {
  4. String prev = reference.getReference();
  5. int prevStamp = reference.getStamp();
  6. System.out.println("原始值 " + prev);
  7. new Thread(() -> {
  8. System.out.println("子线程 A->B:" + reference.compareAndSet(reference.getReference(), "B", reference.getStamp(), reference.getStamp() + 1));
  9. System.out.println("子线程 B->A:" + reference.compareAndSet(reference.getReference(), "A", reference.getStamp(), reference.getStamp() + 1));
  10. }).start();
  11. Thread.sleep(1000);
  12. System.out.println("主线程 A->C:" + reference.compareAndSet(prev, "C", prevStamp, prevStamp + 1));
  13. }
  14. }

原始值:A

子线程 A->B:true

子线程 B->A:true

主线程 A->C:false

【标记解决 ABA 问题】

  1. public class ThreadTest {
  2. private static final AtomicMarkableReference<String> reference = new AtomicMarkableReference<>("A", false);
  3. public static void main(String[] args) throws InterruptedException {
  4. String prev = reference.getReference();
  5. boolean marked = reference.isMarked();
  6. System.out.println("原始值 " + prev);
  7. new Thread(() -> {
  8. System.out.println("子线程 A->B:" + reference.compareAndSet(reference.getReference(), "B", reference.isMarked(), true));
  9. System.out.println("子线程 B->A:" + reference.compareAndSet(reference.getReference(), "A", reference.isMarked(), true));
  10. }).start();
  11. Thread.sleep(1000);
  12. System.out.println("主线程 A->C:" + reference.compareAndSet(prev, "C", marked, !marked));
  13. }
  14. }

原始值:A

子线程 A->B:true

子线程 B->A:true

主线程 A->C:false