【Java进阶笔记】synchronized原理(悲观锁和乐观锁)

959 阅读11分钟

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. 常见的线程安全类

StringIntegerStringBufferRandomVecatorHashTablejava.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() 方法

  1. method1() 尝试对锁对象加锁:
  • 虚拟机在当前线程(Main Thread)的栈帧中建立一个锁记录(Lock Record)的空间,包含锁记录自身的地址指向锁对象的指针
  • 尝试用 CAS 把锁记录自身的地址锁对象的Mark Word进行交换,由于锁对象的状态为无锁(01),因此交换成功后状态将变为轻量级锁(00)。
  1. method2() 尝试对锁对象加锁: 但由于锁对象的状态为为轻量级锁(00)且锁对象的 Mark Word 指向当前线程(Main Thread)。因此执行锁重入,则新增一条锁记录作为重入的计数。

  2. method2() 尝试对锁对象解锁: 锁记录的地址为 NULL ,表示有重入,只需要移除锁记录,从而使重入计数减1。

  3. method1() 尝试对锁对象解锁: 锁记录的地址不为 NULL ,尝试用 CAS 把锁对象的Mark Word恢复为对象头。因此交换成功后状态将变为无锁(01)。

1.4.2. 重量级锁

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

private static final Object obj = new Object();

public static void method(){
    synchronized (obj){	// ①

    }			// ②
}

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

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

于是进入锁膨胀流程:

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

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() 是一个线程等待另一个线程运行结束。

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. 原子类

AtomicBooleanAtomicIntegerAtomicLong

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