手机版网站 html5,wordpress网站 华为,wordpress 视频播放大小,it外包项目都在哪接的JavaEE#xff1a;多线程进阶 一、对比不同锁策略之间的应用场景及其区别1. 悲观锁 和 乐观锁1.1 定义和原理1.2 应用场景1.3 示例代码 2. 重量级锁 和 轻量级锁2.1 定义和原理2.2 应用场景2.3 示例代码 3. 挂起等待锁 和 自旋锁3.1 定义和原理3.2 应用场景3.3 示例代码 4. 几… JavaEE多线程进阶 一、对比不同锁策略之间的应用场景及其区别1. 悲观锁 和 乐观锁1.1 定义和原理1.2 应用场景1.3 示例代码 2. 重量级锁 和 轻量级锁2.1 定义和原理2.2 应用场景2.3 示例代码 3. 挂起等待锁 和 自旋锁3.1 定义和原理3.2 应用场景3.3 示例代码 4. 几对锁之间的联系5. 总结 二、synchronized 的优化策略1. 锁升级Lazy Initialization1.1 定义和原理1.2 锁的状态转换图1.3 应用场景1.4 示例代码 2. 锁消除Lock Elimination2.1 定义和原理2.2 示例代码 3. 锁粗化Lock Coarsening3.1 定义和原理3.2 示例代码 4. 几种优化策略的联系 三、锁策略21. 可重入锁 和 不可重入锁1.1 定义和原理1.2 应用场景1.3 示例代码1.4 图文并茂1. 初始状态线程 A 获取锁2. 递归调用线程 A 再次尝试获取锁3. 结束状态线程 A 释放锁 2. 公平锁 和 非公平锁2.1 定义和原理2.2 应用场景2.3 示例代码2.4 图文并茂1. 公平锁流程图阅读顺序 2. 非公平锁流程图阅读顺序 3. 总结 四、CASCompare-And-Swap问题1. 通过伪代码逐步引出 CAS 问题1.1 简单变量更新1.2 引入无锁更新的想法1.3 引入比较-交换的思想1.4 引出 CAS 操作的伪代码1.5 讨论 CAS 操作的基本特性1.6 总结 CAS 操作的应用场景 2. 通过 CAS 操作实现原子类2.1 定义问题2.2 引入无锁更新的想法2.3 引出 CAS 操作的伪代码2.4 实现真实的 CAS 操作2.5 分析代码的工作原理2.6 图文并茂说明2.7 总结 3. 通过 CAS 操作实现自旋锁3.1 定义问题3.2 引入无锁更新的想法3.3 引出 CAS 操作的伪代码3.4 实现真实的 CAS 操作3.5 分析代码的工作原理3.6 图文并茂说明3.7 总结 4. CAS 中 ABA 问题4.1 详细介绍 CAS 中的 ABA 问题4.2 辅助代码示例4.3 图文并茂理解 ABA 问题4.4 解决 ABA 问题的方法4.5 总结 一、对比不同锁策略之间的应用场景及其区别
在 Java 中锁策略的选择对于多线程程序的性能和正确性至关重要。不同的锁策略适用于不同的应用场景理解它们的区别和联系有助于编写高效且线程安全的代码。
1. 悲观锁 和 乐观锁
1.1 定义和原理
悲观锁Pessimistic Locking 假设冲突会发生因此在每次访问共享资源都加锁确保只有一个线程能够访问到资源。典型的实现包括 synchronized 和 ReentrantLock 。乐观锁Optimistic Locking假设冲突不会发生只有在真正发生冲突的时候才采取措施。乐观锁通常通过版本号和时间戳来检测冲突。典型实现包括 CASCompare And Swap操作。
1.2 应用场景
悲观锁适用于高并发环境的写操作频繁的场景。例如数据库事务、文件系统等。乐观锁适用于读操作远多于写操作场景。例如缓存分布式系统中的数据一致性。
1.3 示例代码
// 悲观锁示例使用 synchronized
public class PessimisticLockExample {private int count 0;public synchronized void increment() {count;}public static void main(String[] args) throws InterruptedException {PessimisticLockExample example new PessimisticLockExample();Thread t1 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});Thread t2 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final count: example.count);}
}// 乐观锁示例使用 Atomic 类
import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockExample {private AtomicInteger count new AtomicInteger(0);public void increment() {count.incrementAndGet();}public static void main(String[] args) throws InterruptedException {OptimisticLockExample example new OptimisticLockExample();Thread t1 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});Thread t2 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final count: example.count.get());}
}2. 重量级锁 和 轻量级锁
2.1 定义和原理
重量级锁Heavyweight Locking基于操作系统级别的同步机制如 mutex当多个线程竞争同一把锁时线程就会挂起等待和上下文切换开销较大。轻量级锁Lightweight Locking通过 CAS 操作尝试获取锁避免重量级锁的开销。轻量级锁适用于低竞争的场景。
2.2 应用场景
重量级锁 适用于高竞争的场景。例如数据库连接池和文件系统等。轻量级锁 适用于低竞争的场景。例如缓存和计数器等。
2.3 示例代码
// 重量级锁示例使用 ReentrantLock
import java.util.concurrent.locks.ReentrantLock;public class HeavyweightLockExample {private final ReentrantLock lock new ReentrantLock();private int count 0;public void increment() {lock.lock();try {count;} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {HeavyweightLockExample example new HeavyweightLockExample();Thread t1 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});Thread t2 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final count: example.count);}
}// 轻量级锁示例使用 Atomic 类
import java.util.concurrent.atomic.AtomicInteger;public class LightweightLockExample {private AtomicInteger count new AtomicInteger(0);public void increment() {count.incrementAndGet();}public static void main(String[] args) throws InterruptedException {LightweightLockExample example new LightweightLockExample();Thread t1 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});Thread t2 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final count: example.count.get());}
}3. 挂起等待锁 和 自旋锁
3.1 定义和原理
挂起等待锁Blocking Lock当线程无法获取锁时就会挂起并进入等待状态直到锁释放。适用于长时间持有锁的场景。自旋锁Spin Lock当线程无法获取锁时会不断地尝试获取锁并不会进入等待状态。适用于短时间持有锁的场景。
3.2 应用场景
挂起等待锁适用于锁持有时间长的场景。例如数据库查询文件处理等。自旋锁适用于锁持有时间短的场景。例如缓存更新、计数器增加等。
3.3 示例代码
// 挂起等待锁示例使用 ReentrantLock
import java.util.concurrent.locks.ReentrantLock;public class BlockingLockExample {private final ReentrantLock lock new ReentrantLock();private int count 0;public void increment() {lock.lock();try {count;} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {BlockingLockExample example new BlockingLockExample();Thread t1 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});Thread t2 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final count: example.count);}
}// 自旋锁示例使用 SpinLock
public class SpinLock {private boolean isLocked false;public synchronized void lock() {while (isLocked) {// 自旋等待}isLocked true;}public synchronized void unlock() {isLocked false;}
}public class SpinLockExample {private final SpinLock lock new SpinLock();private int count 0;public void increment() {lock.lock();try {count;} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {SpinLockExample example new SpinLockExample();Thread t1 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});Thread t2 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final count: example.count);}
}4. 几对锁之间的联系
悲观锁 VS 乐观锁悲观锁适用于高竞争场景乐观锁适用于低竞争场景。两者都可以使用版本号和 CAS 等机制来结合使用。重量级锁 VS 轻量级锁重量级锁适用于高竞争场景轻量级锁适用于低竞争场景。JVM 的子自适应锁机制根据实际情况动态调整锁的状态。挂起等待锁 VS 自旋锁挂起等待锁适用于锁持有时间长的场景自旋锁适用于锁持有时间短的场景。两者可以根据锁持有的时间来选择使用。
5. 总结
悲观锁适用于高竞争场景线程安全但开销较大。乐观锁适用于低竞争场景通过版本号或 CAS 操作减少开销。重量级锁适用于高竞争场景基于系统级别的同步机制。轻量级锁适用于低竞争场景通过 CAS 操作减少开销。挂起等待锁适用于长时间持有锁的场景减少 CPU 的 使用。自旋锁适用于短时间持有锁的场景避免线程切换开销。
二、synchronized 的优化策略
在 Java 中synchronized 关键字是实现线程同步的重要工具。为了提高 synchronized 的性能JVM 提供了多种优化策略包括锁优化锁消除锁粗化。这些优化策略可以显著减少锁的开销提高多线程程序的性能。
1. 锁升级Lazy Initialization
1.1 定义和原理
锁升级指 JVM 根据锁竞争情况动态调整锁的状态。从偏向锁到轻量级锁再到重量级锁的过程。这种机制类似于懒汉模式的思想即在不需要时尽量做到轻量级的状态只有在竞争加剧的时候才转化为重量级锁。
无锁状态Unlocked对象没有被任何线程锁定。偏向锁状态Biased Locking加入只有一个线程会访问该对象因此不需要每次都加锁。偏向锁通过在对象头中加上线程 ID 来实现。轻量级锁状态Lightweight Locking当有多个线程竞争同一把锁时偏向锁失效进入轻量级锁状态。轻量级锁尝试通过 CASCompare And Swap操作来获取锁避免重量级锁的开销。重量级锁状态Heavyweight Locking当竞争进一步加剧的时候CAS 操作失败的次数过多锁会膨胀为重量级锁进入操作系统级别的同步机制。
1.2 锁的状态转换图
------------------- ------------------- ------------------- -------------------
| | CAS | | CAS | | Fail | |
| Unlocked ------ Biased Lock ------ Lightweight Lock ------ Heavyweight Lock|
| | | | | | | |
------------------- ------------------- ------------------- -------------------1.3 应用场景
锁升级适用于各种需要同步的场景特别是在低竞争情况下能够显著提高性能。
如果一个锁长时间没有竞争JVM 会选择偏向锁或轻量级锁减少加锁的开销。如果锁竞争变得激烈JVM 会自动将锁升级为重量级锁确保线程安全。
1.4 示例代码
public class LockUpgradeExample {private int count 0;public synchronized void increment() {count;}public static void main(String[] args) throws InterruptedException {LockUpgradeExample example new LockUpgradeExample();Thread t1 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});Thread t2 new Thread(() - {for (int i 0; i 10000; i) {example.increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final count: example.count);}
}2. 锁消除Lock Elimination
2.1 定义和原理
锁消除是指编译器在运行代码时自动识别代码识别出一些不必要的同步操作将其优化掉。这可以通过逃逸分析Escape Analysis来实现。即分析对象是否被其他线程访问如果没有就安全的消除同步操作。
2.2 示例代码
public class LockEliminationExample {public static void main(String[] args) {long startTime System.currentTimeMillis(); // 当前电脑系统的时间for (int i 0; i 10000000; i) {performTask();}long endTime System.currentTimeMillis();System.out.println(Time taken: (endTime - startTime) ms);}public static void performTask() {StringBuilder sb new StringBuilder();sb.append(Hello);sb.append(World);// sb 对象仅在当前方法内使用不会被其他线程访问}
}在这个例子中StringBuilder 只在 performTask 方法中使用不会被其他线程访问到JVM 可以通过逃逸分析识别出这个对象不需要同步分析因此进行锁消除。
未优化前每次创建 StringBuilder 都需要同步操作。 优化后通过逃逸分析识别出对象不会被其他线程访问消除同步操作。
3. 锁粗化Lock Coarsening
3.1 定义和原理
锁粗化是指将一系列连续的细粒度的加锁操作合并成一次粗粒度的加锁操作从而减少锁的开销。锁粗化通常应用在循环体内存在多次加锁操作的情况。
3.2 示例代码
public class LockCoarseningExample {private final Object lock new Object();private int sum 0;public void add(int value) {synchronized (lock) {sum value;}}public static void main(String[] args) throws InterruptedException {LockCoarseningExample example new LockCoarseningExample();Thread t1 new Thread(() - {for (int i 0; i 10000; i) {example.add(i);}});Thread t2 new Thread(() - {for (int i 0; i 10000; i) {example.add(i);}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final sum: example.sum);}
}在这个例子中add 方法在循环体内被多次调用每次调用都需要进行同步操作。JVM 通过锁粗化将多次加锁操作合并成一次粗粒度的加锁操作从而减少锁的开销。
未优化前每次调用 add 方法都需要进行加锁操作导致大量的上下文切换。优化后通过锁粗化将多次加锁操作合并成一次粗粒度的加锁操作减少上下文切换。
4. 几种优化策略的联系
锁升级通过动态调整锁的状态从偏向锁到轻量级锁再到重量级锁减少锁的开销适用于多种同步的场景特别是低竞争情况下能够显著提升性能。锁消除通过逃逸分析识别出不必要的同步操作并将其优化掉。适用于那些在本地方法中创建的对象且这些对象不会被其他线程访问到的情况。锁粗化通过多次加锁操作合成一次粗粒度的加锁操作减少锁的开销。适用于那些在循环体内存在多次加锁的操作场景。
三、锁策略2
1. 可重入锁 和 不可重入锁
1.1 定义和原理 可重入锁Reentrant Lock允许同一个线程多次获取同一把锁。每次获取锁时都会增加一个计数器每次释放锁时都会减少计数器只有当计数器归零时才真正释放锁。 不可重入锁Non-reentrant Lock不允许同一个线程多次获取同一把锁。如果一个线程已经持有了锁再次尝试获取锁会导致死锁。
1.2 应用场景 可重入锁适用于递归调用或需要在多个方法中使用同一把锁的场景。 不可重入锁适用于简单的同步控制场景避免了不必要的复杂性和开销。
1.3 示例代码
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;public class ReentrantLockExample {private final Lock lock new ReentrantLock();public void method1() {lock.lock();try {System.out.println(Method 1 acquired the lock.);method2(); // 递归调用} finally {lock.unlock();}}public void method2() {lock.lock();try {System.out.println(Method 2 acquired the lock.);} finally {lock.unlock();}}public static void main(String[] args) {ReentrantLockExample example new ReentrantLockExample();example.method1();}
}在这个例子中method1 调用了 method2由于使用的是 ReentrantLock同一个线程可以多次获取锁而不会导致死锁。
1.4 图文并茂
------------------- ------------------- -------------------
| | | | | |
| Thread A ----- | Acquire Lock ----- | Enter Critical |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Recursive Call ------ Acquire Again ------ Execute Code |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return Success ------- Release Lock ------- Finish Task |
| | | | | |
------------------- ------------------- -------------------1. 初始状态线程 A 获取锁
------------------- ------------------- -------------------
| | | | | |
| Thread A ----- | Acquire Lock ----- | Enter Critical |
| | | | | |
------------------- ------------------- -------------------Thread A表示当前正在执行的线程。Acquire Lock线程 A 尝试获取锁。由于使用的是可重入锁如 ReentrantLock如果锁没有被其他线程持有线程 A 可以成功获取锁。Enter Critical一旦成功获取锁线程 A 进入临界区Critical Section可以安全地访问共享资源。
2. 递归调用线程 A 再次尝试获取锁
------------------- ------------------- -------------------
| | | | | |
| Recursive Call ------ Acquire Again ------ Execute Code |
| | | | | |
------------------- ------------------- -------------------Recursive Call线程 A 在临界区内调用了另一个方法而该方法也需要获取同一把锁。由于使用的是可重入锁线程 A 可以再次成功获取锁。Acquire Again线程 A 再次尝试获取锁由于是同一个线程锁的计数器增加允许再次获取锁。Execute Code线程 A 继续执行代码逻辑访问共享资源。
3. 结束状态线程 A 释放锁
------------------- ------------------- -------------------
| | | | | |
| Return Success ------- Release Lock ------- Finish Task |
| | | | | |
------------------- ------------------- -------------------Finish Task线程 A 完成所有任务准备退出临界区。Release Lock线程 A 释放锁。每次释放锁时锁的计数器减少一次。如果计数器归零则真正释放锁允许其他线程获取锁。Return Success线程 A 成功释放锁并返回结果。
2. 公平锁 和 非公平锁
2.1 定义和原理 公平锁Fair Lock按照请求顺序获取锁即先请求的线程先获得锁。这种机制避免了“饥饿”现象但可能导致较高的等待时间。 非公平锁Non-fair Lock不保证请求顺序任何线程都有机会获取锁。这种机制通常具有更高的吞吐量但可能导致某些线程长时间得不到锁。
2.2 应用场景 公平锁适用于对响应时间要求较高、不能容忍“饥饿”现象的场景。 非公平锁适用于对吞吐量要求较高、可以容忍一定延迟的场景。
2.3 示例代码
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;public class FairAndUnfairLockExample {private static final Lock fairLock new ReentrantLock(true); // 公平锁private static final Lock unfairLock new ReentrantLock(false); // 非公平锁public static void main(String[] args) throws InterruptedException {Runnable task () - {try {fairLock.lock();try {System.out.println(Thread.currentThread().getName() acquired fair lock.);Thread.sleep(1000); // 模拟长时间持有锁} finally {fairLock.unlock();}} catch (InterruptedException e) {e.printStackTrace();}};for (int i 0; i 5; i) {Thread thread new Thread(task, Thread- i);thread.start();}Thread.sleep(6000); // 等待所有线程完成task () - {try {unfairLock.lock();try {System.out.println(Thread.currentThread().getName() acquired unfair lock.);Thread.sleep(1000); // 模拟长时间持有锁} finally {unfairLock.unlock();}} catch (InterruptedException e) {e.printStackTrace();}};for (int i 0; i 5; i) {Thread thread new Thread(task, Thread- i);thread.start();}}
}在这个例子中我们创建了两个锁实例一个是公平锁另一个是非公平锁。通过观察输出结果可以看到公平锁按照请求顺序获取锁而非公平锁则不一定。
2.4 图文并茂
------------------- ------------------- -------------------
| | | | | |
| Threads ----- | Request Lock ----- | Wait in Queue |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Fair Lock ------ Grant Lock ------ Execute Code |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return Success ------- Release Lock ------- Finish Task |
| | | | | |
------------------- ------------------- -------------------------------------- ------------------- -------------------
| | | | | |
| Threads ----- | Request Lock ----- | Random Order |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Non-fair Lock ------ Grant Lock ------ Execute Code |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return Success ------- Release Lock ------- Finish Task |
| | | | | |
------------------- ------------------- -------------------1. 公平锁流程图
------------------- ------------------- -------------------
| | | | | |
| Threads ----- | Request Lock ----- | Wait in Queue |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Fair Lock ------ Grant Lock ------ Execute Code |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return Success ------- Release Lock ------- Finish Task |
| | | | | |
------------------- ------------------- -------------------阅读顺序 从左到右 第一列表示多个线程Threads。第二列表示线程请求锁Request Lock。第三列表示线程进入等待队列Wait in Queue。第四列表示线程执行代码Execute Code。 从上到下 线程首先尝试请求锁Request Lock如果锁已经被其他线程持有则进入等待队列Wait in Queue。当前持有锁的线程释放锁后公平锁会按照请求顺序依次授予锁Grant Lock即先请求的线程先获得锁。获得锁的线程开始执行临界区代码Execute Code。执行完临界区代码后线程释放锁Release Lock并返回成功Return Success。
2. 非公平锁流程图
------------------- ------------------- -------------------
| | | | | |
| Threads ----- | Request Lock ----- | Random Order |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Non-fair Lock ------ Grant Lock ------ Execute Code |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return Success ------- Release Lock ------- Finish Task |
| | | | | |
------------------- ------------------- -------------------阅读顺序 从左到右 第一列表示多个线程Threads。第二列表示线程请求锁Request Lock。第三列表示线程以随机顺序等待Random Order。第四列表示线程执行代码Execute Code。 从上到下 线程首先尝试请求锁Request Lock如果锁已经被其他线程持有则进入等待状态Random Order但不保证按请求顺序排队。当前持有锁的线程释放锁后非公平锁可能会优先授予刚刚释放锁的线程或其他正在请求锁的线程Grant Lock而不一定按照请求顺序。获得锁的线程开始执行临界区代码Execute Code。执行完临界区代码后线程释放锁Release Lock并返回成功Return Success。
3. 总结 可重入锁 vs 不可重入锁 可重入锁允许同一个线程多次获取同一把锁适用于递归调用或需要在多个方法中使用同一把锁的场景。不可重入锁不允许同一个线程多次获取同一把锁适用于简单的同步控制场景。 公平锁 vs 非公平锁 公平锁按照请求顺序获取锁适用于对响应时间要求较高、不能容忍“饥饿”现象的场景。非公平锁不保证请求顺序任何线程都有机会获取锁适用于对吞吐量要求较高、可以容忍一定延迟的场景。
四、CASCompare-And-Swap问题
1. 通过伪代码逐步引出 CAS 问题
我们将从一个简单的场景开始逐步引出 CAS 操作的概念和其潜在的问题。
伪代码
boolean CAS(address, expectValue, swapValue) {if (address expectValue) {address swapValue;return true;}return false;
}1.1 简单变量更新
假设我们有一个共享的变量 value 多个线程要对其进行更新操作。最直接的方法是对其进行加锁操作保证线程安全。
public class SimpleUpdate {private int value 0;public synchronized void update(int newValue) {value newValue;}public static void main(String[] args) throws InterruptedException {SimpleUpdate updater new SimpleUpdate();Thread t1 new Thread(() - updater.update(1));Thread t2 new Thread(() - updater.update(2));t1.start();t2.start();t1.join();t2.join();System.out.println(Final Value: updater.value);}
}在这个例子中我们使用 synchronized 关键字来确保只有一个线程对共享变量 value 进行更新操作从而避免竞争条件。
1.2 引入无锁更新的想法
虽然锁机制可以确保线程安全但它也有一定的开销特别在高并发环境下。因此我们可以考虑一种无锁的更新方法。下面是最初的尝试
public class NaiveUpdate {private int value 0;public void update(int newValue) {// 直接更新值value newValue;}public static void main(String[] args) throws InterruptedException {NaiveUpdate updater new NaiveUpdate();Thread t1 new Thread(() - updater.update(1));Thread t2 new Thread(() - updater.update(2));t1.start();t2.start();t1.join();t2.join();System.out.println(Final Value: updater.value);}
}这种方法存在明显的竞争条件问题。两个线程可能同时读取和更新 value 导致结果不可预测。
1.3 引入比较-交换的思想
为了确保线程安全我们需要在更新之前检查当前值value是否与预期值expectedValue一致。如果一致就进行更新操作如果不一致就不做任何处理。这正是 CAS 操作的核心思想。
public class CompareAndSwapUpdate {private int value 0;public boolean compareAndSwap(int expectedValue, int swapValue) {if (value expectedValue) {value swapValue;return true;}return false;}public static void main(String[] args) throws InterruptedException {CompareAndSwapUpdate updater new CompareAndSwapUpdate();Thread t1 new Thread(() - {while (!updater.compareAndSwap(0, 1)) {// 自旋等待}});Thread t2 new Thread(() - {while (!updater.compareAndSwap(0, 2)) {// 自旋等待}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final Value: updater.value);}
}在这个例子中定义了一个 compareAndSwap 方法首先检查 value 是否和 expectedValue 相等。如果相等就将 value 更新为 swapValue 并返回 true 。如果不相等不进行任何操作并返回 false 。每个线程都在不断地尝试更新直到成功为止。
1.4 引出 CAS 操作的伪代码
回到最初的伪代码
boolean CAS(address, expectValue, swapValue) {if (address expectValue) {address swapValue;return true;}return false;
}在这段伪代码中
address 要修改的内存地址expectValue 当前预期的值。swapValue 要更新的新值。如果 address 的值等于 expectValue则将其更新为 swapValue 并返回 true否则返回 false 。
1.5 讨论 CAS 操作的基本特性
原子性CAS 操作是一种原子操作意味着它在硬件层面上是不可分割的不会被其他线程打断。无锁编程CAS 操作不需要显示的锁因此可以避免死锁的开销和上下文切换。高效性在低竞争的情况下CAS 操作比锁机制更加高效因为它直接在硬件级别上进行原子操作。
1.6 总结 CAS 操作的应用场景
计数器例如 AtomicInteger 类中的自增或自减。缓存用于更新缓存中的数据。队列用于实现无锁队列等数据结构。
2. 通过 CAS 操作实现原子类
我们将通过一段伪代码逐步引出如何使用 CAS 操作来实现简单的原子类并详细分析其工作原理和应用场景。
伪代码
class AtomicInteger { private int value; public int getAndIncrement() { int oldValue value; while ( CAS(value, oldValue, oldValue 1) ! true) { oldValue value; } return oldValue; }
}2.1 定义问题
假设我们需要实现一个线程安全的计数器支持自增操作最直接的办法是使用锁机制
public class SimpleCounter {private int value 0;public synchronized int getAndIncrement() {return value;}public static void main(String[] args) throws InterruptedException {SimpleCounter counter new SimpleCounter();Thread t1 new Thread(() - {for (int i 0; i 1000; i) {counter.getAndIncrement();}});Thread t2 new Thread(() - {for (int i 0; i 1000; i) {counter.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final Value: counter.value);}
}在这个例子中使用 synchronized 关键字确保只有一个线程能够访问到 getAndIncrement 方法从而避免竞争条件。然而这种方式需要一定的开销特别是在高并发环境下。
2.2 引入无锁更新的想法
为了提高性能我们考虑使用一种无锁的更新机制。CASCompare-And-Swap操作可以实现这一点。下面是我们的初步尝试
public class NaiveAtomicInteger {private int value 0;public boolean compareAndSwap(int expectedValue, int newValue) {if (value expectedValue) {value newValue;return true;}return false;}public static void main(String[] args) throws InterruptedException {NaiveAtomicInteger atomicInteger new NaiveAtomicInteger();Thread t1 new Thread(() - {int oldValue atomicInteger.value;while (!atomicInteger.compareAndSwap(oldValue, oldValue 1)) {oldValue atomicInteger.value;}});Thread t2 new Thread(() - {int oldValue atomicInteger.value;while (!atomicInteger.compareAndSwap(oldValue, oldValue 1)) {oldValue atomicInteger.value;}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final Value: atomicInteger.value);}
}在这个例子中我们定义了一个 compareAndSwap 方法它首先检查 value 是否等于 expectedValue如果是则将其更新为 newValue 并返回 true否则返回 false。每个线程在一个循环中不断尝试更新直到成功为止。
2.3 引出 CAS 操作的伪代码
回到最初的伪代码
class AtomicInteger { private int value; public int getAndIncrement() { int oldValue value; while ( CAS(value, oldValue, oldValue 1) ! true) { oldValue value; } return oldValue; }
}在这段伪代码中
value 是要修改的共享变量。oldValue 是当前读取到的变量。CAS(value, oldValue, oldValue 1) 这是一个原子操作。第一步先判断 oldValue 和 value 是否相等。假如相等就将其更新为 oldValue 1 并返回 true否则返回 false 。
2.4 实现真实的 CAS 操作
在 Java 中可以通过 sun.misc.Unsafe 类或 java.util.concurrent.atomic 包中的类来实现。下面是使用 AtomicInteger 实现的完整示例
import java.util.concurrent.atomic.AtomicInteger;public class AtomicCounter {private final AtomicInteger value new AtomicInteger(0);public int getAndIncrement() {return value.getAndIncrement();}public static void main(String[] args) throws InterruptedException {AtomicCounter counter new AtomicCounter();Thread t1 new Thread(() - {for (int i 0; i 1000; i) {counter.getAndIncrement();}});Thread t2 new Thread(() - {for (int i 0; i 1000; i) {counter.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Final Value: counter.value.get());}
}在这个例子中我们使用 AtomicInteger 类来实现一个线程安全的计数器。AtomicInteger 类内部实现了 CAS 操作确保多个线程可以安全的进行自增操作。
2.5 分析代码的工作原理
详细分析一下 getAndIncrement 方法的工作原理
读取当前值首先读取 value 的值并保存到 oldValue 中。CAS 操作尝试将 value 更新为 oldValue 1 。如果 value 等于 oldValue更新成功就返回 true否则就返回 false 。重试机制如果 CAS 操作失败value 值在此期间被其他线程修改则重新读取 value 并再次尝试 CAS 操作直到成功为止。返回旧值成功之后返回最初的值 oldValue 即使在此期间 value 被多次修改。
2.6 图文并茂说明
图解 CAS 操作流程
------------------- ------------------- -------------------
| | | | | |
| Current Value ----- | Expected Value ----- | New Value |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Compare ------ If Equal ------ Set New Value |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return True ------- Update Success ------- Return False |
| | | | | |
------------------- ------------------- -------------------自旋等待机制
------------------- ------------------- -------------------
| | | | | |
| Read Value ----- | CAS Operation ----- | Retry if Failed|
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return Old ------- Update Success ------- Read Again |
| | | | | |
------------------- ------------------- -------------------2.7 总结
通过上述步骤我们逐步引出了如何使用 CAS 操作来实现一个简单的原子类并详细分析了其工作原理和应用场景。CAS 操作的核心思想是通过比较和交换实现无锁编程确保多线程环境下的安全性。尽管 CAS 操作有一些潜在的问题如 ABA 问题和自旋开销但在低竞争情况下它通常比锁机制更高效。
3. 通过 CAS 操作实现自旋锁
我们将通过一段伪代码逐步引出自旋锁的概念并详细分析其工作原理和应用场景。
伪代码
public class SpinLock {private Thread owner null;public void lock() {// 通过 CAS 看当前锁是否被某个线程持有。// 如果这个锁已经被别的线程持有那么就自旋等待。// 如果这个锁没有被别的线程持有那么就把 owner 设置为当前尝试加锁的线程。while (!CAS(this.owner, null, Thread.currentThread())) {}}public void unlock() {this.owner null;}
}3.1 定义问题
假设我们需要一个简单的互斥锁Mutex确保只有一个线程在同一时间内访问共享资源。最直接的方法是使用显性的锁机制。
public class SimpleLock {private boolean isLocked false;public synchronized void lock() {while (isLocked) {try {wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}isLocked true;}public synchronized void unlock() {isLocked false;notifyAll();}public static void main(String[] args) throws InterruptedException {SimpleLock lock new SimpleLock();Thread t1 new Thread(() - {lock.lock();try {System.out.println(Thread 1 acquired the lock.);Thread.sleep(1000); // 模拟长时间持有锁} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();System.out.println(Thread 1 released the lock.);}});Thread t2 new Thread(() - {lock.lock();try {System.out.println(Thread 2 acquired the lock.);} finally {lock.unlock();System.out.println(Thread 2 released the lock.);}});t1.start();t2.start();t1.join();t2.join();}
}在这个例子中通过 synchronized 关键字来实现只有一个线程在一个时间段内访问到共享资源在锁被释放是唤醒等待的线程但是这种方法存在一定的开销尤其在高并发的环境下。
3.2 引入无锁更新的想法
为了提高性能我们可以考虑使用一种无锁的更新方法。CASCompare-And-Swap操作可以帮助我们实现这一点。下面是我们的初步尝试
public class NaiveSpinLock {private Thread owner null;public void lock() {Thread currentThread Thread.currentThread();while (owner ! null owner ! currentThread) {// 自旋等待}owner currentThread;}public void unlock() {owner null;}public static void main(String[] args) throws InterruptedException {NaiveSpinLock spinLock new NaiveSpinLock();Thread t1 new Thread(() - {spinLock.lock();try {System.out.println(Thread.currentThread().getName() acquired the lock.);Thread.sleep(1000); // 模拟长时间持有锁} catch (InterruptedException e) {e.printStackTrace();} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() released the lock.);}});Thread t2 new Thread(() - {spinLock.lock();try {System.out.println(Thread.currentThread().getName() acquired the lock.);} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() released the lock.);}});t1.start();Thread.sleep(100); // 确保 t1 先开始t2.start();t1.join();t2.join();}
}在这个例子中我们定义了一个 lock 方法它首先检查当前锁是否被其他线程持有 owner ! null 并且判断当前线程是否已经是获取到锁对象的线程owner ! currentThread。如果是就将 owner 设置成当前线程否则进入自旋等待状态。每个线程在一个循环中不断地尝试获取锁直到成功为止。
3.3 引出 CAS 操作的伪代码
现在让我们回到最初提供的伪代码并解释它的含义
owner 表示当前持有锁的线程。CAS(this.owner, null, Thread.currentThread()) 是一个原子操作它首先检查 owner 是否为 null检查锁是否已经被其他线程持有。如果是则将其更新为当前线程并返回 true否则返回 false。
3.4 实现真实的 CAS 操作
在 Java 中CAS 操作可以通过 sun.misc.Unsafe 类或 java.util.concurrent.atomic 包中的类来实现。下面是一个使用 AtomicReference 实现的完整示例
import java.util.concurrent.atomic.AtomicReference;public class SpinLock {private final AtomicReferenceThread owner new AtomicReference();public void lock() {Thread currentThread Thread.currentThread();// 自旋等待直到成功获取锁while (!owner.compareAndSet(null, currentThread)) {// 可以在这里添加一些优化例如短暂休眠以减少 CPU 开销// Thread.yield(); 或者 LockSupport.parkNanos(1L);}}public void unlock() {Thread currentThread Thread.currentThread();if (owner.get() currentThread) {owner.set(null);} else {throw new IllegalMonitorStateException(Calling thread does not hold the lock);}}public static void main(String[] args) throws InterruptedException {SpinLock spinLock new SpinLock();Thread t1 new Thread(() - {spinLock.lock();try {System.out.println(Thread.currentThread().getName() acquired the lock.);Thread.sleep(1000); // 模拟长时间持有锁} catch (InterruptedException e) {e.printStackTrace();} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() released the lock.);}});Thread t2 new Thread(() - {spinLock.lock();try {System.out.println(Thread.currentThread().getName() acquired the lock.);} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() released the lock.);}});t1.start();Thread.sleep(100); // 确保 t1 先开始t2.start();t1.join();t2.join();}
}在这个例子中使用 AtomicReference 类来实现自旋锁。AtomicReference 内部实现了 CAS 操作确保多个线程能够安全地进行锁操作。
3.5 分析代码的工作原理
详细分析一下 lock 和 unlock 方法的工作原理。
读取当前值首先读取 owner 的值。CAS 操作尝试将 owner 更新为当前线程如果当前的 owner 为 null则更新成功并返回 true否则返回 false 。重试机制如果 CAS 操作失败即 owner 在此期间被其他线程修改则重新读取当前的 owner 并再次尝试 CAS 操作直到成功为止。解除操作当线程完成任务后调用 unlock 方法将 owner 设置为 null允许其他线程获取锁。
3.6 图文并茂说明
图解自旋锁流程
------------------- ------------------- -------------------
| | | | | |
| Current Owner ----- | Expected Owner ----- | New Owner |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Compare ------ If Equal ------ Set New Owner |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return True ------- Update Success ------- Return False |
| | | | | |
------------------- ------------------- -------------------自旋等待机制
------------------- ------------------- -------------------
| | | | | |
| Read Owner ----- | CAS Operation ----- | Retry if Failed|
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return Old ------- Update Success ------- Read Again |
| | | | | |
------------------- ------------------- -------------------3.7 总结
通过上述步骤我们逐步引出了如何使用 CAS 操作来实现一个简单的自旋锁并详细分析了其工作原理和应用场景。自旋锁的核心思想是通过不断尝试获取锁适用于短时间持有锁的场景因为它避免了线程上下文切换的开销。尽管自旋锁有一些潜在的问题如高 CPU 开销但在低竞争情况下它通常比锁机制更高效。
4. CAS 中 ABA 问题
4.1 详细介绍 CAS 中的 ABA 问题
什么是 ABA 问题
ABA 问题 Atomicity, Consistency, and Availability是指由于某个线程在读取值后被其他线程修改两次最终又变回原来的值导致的问题。具体说明如下
线程 A 读取 V 的值为 A 。线程 B 修改 V 的值为 B然后将 V 的值改回 A 。当线程 A 检查 V 的值时发现还是 A认为变量没有被修改过。
尽管变量 V 的值看起来没有发生变化但实际上是被修改过的这可能会导致逻辑错误。
4.2 辅助代码示例
下面是一个简单的 Java 示例展示了 ABA 问题的发生过程
import java.util.concurrent.atomic.AtomicInteger;public class ABADemo {private static AtomicInteger value new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {// 线程 A 尝试将 value 自增 1int expectedValue value.get();while (!value.compareAndSet(expectedValue, expectedValue 1)) {expectedValue value.get();}System.out.println(Thread A: Value updated to value.get());});Thread t2 new Thread(() - {// 线程 B 先将 value 设为 1再设回 0value.set(1);value.set(0);System.out.println(Thread B: Value set to 1 then back to 0);});t1.start();Thread.sleep(100); // 确保 t1 先开始t2.start();t1.join();t2.join();System.out.println(Final Value: value.get());}
}在这个例子中线程 A 通过 CAS 操作将 value 自增 1而线程 B 则先将 value 设为 1然后再设回 0。由于线程 A 在检查 value 值的值时发现它还是 0 就继续进行自增操作。但实际上value 值已经被修改过一次了但还是识别不出来这就是 ABA 问题。
4.3 图文并茂理解 ABA 问题
ABA 问题的流程图
------------------- ------------------- -------------------
| | | | | |
| Initial Value ----- | Thread B Modify ----- | Thread B Revert |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Read by Thread A------ Check Value ------ Update Failed |
| | | | | |
------------------- ------------------- -------------------| | |v v v
------------------- ------------------- -------------------
| | | | | |
| Return Old ------- Update Success ------- Read Again |
| | | | | |
------------------- ------------------- -------------------ABA 问题的具体步骤 初始化假设初始值为 value 。线程 A 读取 value 值保存到 expectValue 中。尝试执行 CAS(value, 0, 1) 操作如果成功将 value 更新成 1 如果失败则返回 expectedValue 为 0 。 线程 B先将 value 设置为 1 再将 value 设回 0 。线程 A 继续尝试 读取 value 值保存到 expectValue 中。再次尝试 CAS(value, 0, 1) 操作因为当前 value 值为 0因此操作成功将 value 值更新为 1 。 结果由于 value 值被修改过一次但线程 A 还是无法察觉这一变化继续进行自增操作。
4.4 解决 ABA 问题的方法
为了防止 ABA 问题的发生可以使用带有版本号或时间戳的 CAS 操作。Java 提供了 AtomicStampedReference 类来解决这个问题。
使用 AtomicStampedReference 解决 ABA 问题
import java.util.concurrent.atomic.AtomicStampedReference;public class ABASolution {private static AtomicStampedReferenceInteger value new AtomicStampedReference(0, 0);public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {int[] stampHolder new int[1];Integer currentValue value.get(stampHolder);int currentStamp stampHolder[0];boolean success value.compareAndSet(currentValue, currentValue 1, currentStamp, currentStamp 1);System.out.println(Thread 1: success , New Value: value.getReference());});Thread t2 new Thread(() - {int[] stampHolder new int[1];Integer currentValue value.get(stampHolder);int currentStamp stampHolder[0];value.compareAndSet(currentValue, currentValue 1, currentStamp, currentStamp 1);value.compareAndSet(currentValue 1, currentValue, currentStamp 1, currentStamp 2);System.out.println(Thread 2: Value set to (currentValue 1) then back to currentValue);});t1.start();Thread.sleep(100); // 确保 t1 先开始t2.start();t1.join();t2.join();System.out.println(Final Value: value.getReference());}
}在这个例子中我们使用 AtomicStampedReference 类来跟踪变量的版本号或时间戳。每次修改变量都会更新版本号。这样即使变量的值变回原来的状态版本号也会不一样就从而避免了 ABA 问题。
AtomicStampedReference 的工作原理
AtomicStampedReference 使用一个整数作为版本号与引用一起存储。每次进行 CAS 操作时不仅比较引用的值还比较版本号。只有当引用的值和版本号都匹配时才允许更新。
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {PairV current pair;returnexpectedReference current.reference expectedStamp current.stamp ((newReference current.reference newStamp current.stamp) ||casPair(current, Pair.of(newReference, newStamp)));
}4.5 总结
ABA 问题由于某个线程在读取值后被其他线程修改两次最终又变回原来的值导致的问题。CAS 操作中的 ABA 问题CAS 操作只是让当前的值和预期的值进行比较而没有考虑到中间是否会发生变化这样检查不出 ABA 问题。解决方案使用带版本号或时间戳的 CAS 操作AtomicStampedReference即使变量变回原来的状态版本号不同从而避免 ABA 问题。