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) 。
:star: 避免竞态条件的解决方案:
- 阻塞式: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
。
:star:️ 对 synchronized 的理解: synchronized
实际是用对象锁保证了 临界区 内代码的 原子性 ,临界区内的代码对外是不可分割的,也不会被线程切换所打断。
1.2.1. synchronized 修饰方法
synchronized
修饰成员方法:
class Test { private Object obj = new Object(); // 对成员方法加锁,相当于把该类的所有成员给锁住(obj) public synchronized void test() {} } // 两者在效果上等价 class Test { private Object obj = new Object(); public void test() { // 对this加锁,相当于把该类对象给锁住(Test对象) synchronized (this) {} } } 复制代码
synchronized
修饰静态方法:
class Test { public synchronized static void test() { } } // 两者在效果上等价 class Test { public static void test() { // 静态方法,没有实例对象,只能对类对象加锁(Test.class) synchronized (Test.class) { } } } 复制代码
1.2.2. 变量的线程安全分析
【成员变量和静态变量】
如果没共享,则 线程安全 。
如果被共享,根据是否读写来判断:
- 如果只有读操作,则 线程安全 。
- 如果有读写操作,则这段代码是临界区,线程不安全。
【局部变量】
- 局部变量一般 线程安全 。
- 局部变量引用的对象,根据是否有方法逃逸来判断:
- 如果该对象没有逃离方法的作用访问,则 线程安全 。
- 如果该对象逃离方法的作用范围,则 线程不安全 。
1.2.3. 常见的线程安全类
String
、 Integer
、 StringBuffer
、 Random
、 Vecator
、 HashTable
、 java.util.concurrent
包下的类。
:star:️ 注意:
- 这里的线程安全是指多个线程调用它们同一个实例的某个方法时,是线程安全的。
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 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)的空间,包含
锁记录自身的地址
和指向锁对象的指针
。
- 尝试用 CAS 把
锁记录自身的地址
和锁对象的Mark Word
进行交换,由于锁对象的状态为无锁(01),因此交换成功后状态将变为轻量级锁(00)。
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)已经对该对象加了轻量级锁,因此会加锁失败。
于是 进入锁膨胀 流程:
- 为锁对象申请 Monitor 锁。
- 将锁对象的 Mark Word 指向当前 Monitor ,并把状态置为重量级锁(10)。
- Monitor 的 Owner 则指向
- 其他线程进入 Monitor 的 EntryList 中并变为 BLOCKED 状态。
- 主线程(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. 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞(从而避免上下文切换)。如果自旋超时,则会自旋失败,当前线程就会进入阻塞状态。
:star:️ 注意:
自旋会占用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. 原子类
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