JavaEE:多线程进阶
JavaEE:多线程进阶
一、对比不同锁策略之间的应用场景及其区别
在 Java 中,锁策略的选择对于多线程程序的性能和正确性至关重要。不同的锁策略适用于不同的应用场景,理解它们的区别和联系有助于编写高效且线程安全的代码。
1. 悲观锁 和 乐观锁
1.1 定义和原理
- 悲观锁(Pessimistic Locking) :假设冲突会发生,因此在每次访问共享资源都加锁,确保只有一个线程能够访问到资源。典型的实现包括
synchronized
和ReentrantLock
。 - 乐观锁(Optimistic Locking):假设冲突不会发生,只有在真正发生冲突的时候才采取措施。乐观锁通常通过版本号和时间戳来检测冲突。典型实现包括 CAS(Compare 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):当有多个线程竞争同一把锁时,偏向锁失效,进入轻量级锁状态。轻量级锁尝试通过 CAS(
Compare 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 非公平锁:
- 公平锁:按照请求顺序获取锁,适用于对响应时间要求较高、不能容忍“饥饿”现象的场景。
- 非公平锁:不保证请求顺序,任何线程都有机会获取锁,适用于对吞吐量要求较高、可以容忍一定延迟的场景。
四、CAS(Compare-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 引入无锁更新的想法
为了提高性能,我们考虑使用一种无锁的更新机制。CAS(Compare-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 引入无锁更新的想法
为了提高性能,我们可以考虑使用一种无锁的更新方法。CAS(Compare-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 AtomicReference<Thread> 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 自增 1
int 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,再设回 0
value.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 AtomicStampedReference<Integer> 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) {
Pair<V> current = pair;
return
expectedReference == 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 问题。
原文地址:https://blog.csdn.net/Joseit/article/details/145216989
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!