当前位置: 首页 > news >正文

JavaEE:多线程进阶

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 Initialization)
      • 1.1 定义和原理
      • 1.2 锁的状态转换图
      • 1.3 应用场景
      • 1.4 示例代码
    • 2. 锁消除(Lock Elimination)
      • 2.1 定义和原理
      • 2.2 示例代码
    • 3. 锁粗化(Lock Coarsening)
      • 3.1 定义和原理
      • 3.2 示例代码
    • 4. 几种优化策略的联系
  • 三、锁策略2
    • 1. 可重入锁 和 不可重入锁
      • 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. 总结
  • 四、CAS(Compare-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) :假设冲突会发生,因此在每次访问共享资源都加锁,确保只有一个线程能够访问到资源。典型的实现包括 synchronizedReentrantLock
  • 乐观锁(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     |
|                   |       |                   |       |                   |
+-------------------+       +-------------------+       +-------------------+
阅读顺序:
  1. 从左到右

    • 第一列:表示多个线程(Threads)。
    • 第二列:表示线程请求锁(Request Lock)。
    • 第三列:表示线程进入等待队列(Wait in Queue)。
    • 第四列:表示线程执行代码(Execute Code)。
  2. 从上到下

    • 线程首先尝试请求锁(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     |
|                   |       |                   |       |                   |
+-------------------+       +-------------------+       +-------------------+
阅读顺序:
  1. 从左到右

    • 第一列:表示多个线程(Threads)。
    • 第二列:表示线程请求锁(Request Lock)。
    • 第三列:表示线程以随机顺序等待(Random Order)。
    • 第四列:表示线程执行代码(Execute Code)。
  2. 从上到下

    • 线程首先尝试请求锁(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 操作的应用场景

  1. 计数器:例如 AtomicInteger 类中的自增或自减。
  2. 缓存:用于更新缓存中的数据。
  3. 队列:用于实现无锁队列等数据结构。

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) :这是一个原子操作。第一步,先判断 oldValuevalue 是否相等。假如相等,就将其更新为 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 分析代码的工作原理

详细分析一下 lockunlock 方法的工作原理。

  • 读取当前值:首先读取 owner 的值。
  • CAS 操作:尝试将 owner 更新为当前线程,如果当前的 ownernull,则更新成功并返回 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)是指由于某个线程在读取值后被其他线程修改两次,最终又变回原来的值导致的问题。具体说明如下:

  1. 线程 A 读取 V 的值为 A
  2. 线程 B 修改 V 的值为 B,然后将 V 的值改回 A
  3. 当线程 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 问题

  1. 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      |
|                   |       |                   |       |                   |
+-------------------+       +-------------------+       +-------------------+
  1. ABA 问题的具体步骤
    1. 初始化:假设初始值为 value
    2. 线程 A
    • 读取 value 值,保存到 expectValue 中。
    • 尝试执行 CAS(value, 0, 1) 操作,如果成功,将 value 更新成 1 ,如果失败,则返回 expectedValue 为 0 。
    1. 线程 B:先将 value 设置为 1 ;再将 value 设回 0 。
    2. 线程 A 继续尝试
    • 读取 value 值,保存到 expectValue 中。
    • 再次尝试 CAS(value, 0, 1) 操作,因为当前 value 值为 0,因此操作成功,将 value 值更新为 1 。
    1. 结果:由于 value 值被修改过一次,但线程 A 还是无法察觉这一变化,继续进行自增操作。

4.4 解决 ABA 问题的方法

为了防止 ABA 问题的发生,可以使用带有版本号或时间戳的 CAS 操作。Java 提供了 AtomicStampedReference 类来解决这个问题。

  1. 使用 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 问题。

  1. AtomicStampedReference 的工作原理

AtomicStampedReference 使用一个整数作为版本号,与引用一起存储。每次进行 CAS 操作时,不仅比较引用的值,还比较版本号。只有当引用的值和版本号都匹配时,才允许更新。

public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {Pair<V> 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 问题。

相关文章:

JavaEE:多线程进阶

JavaEE&#xff1a;多线程进阶 一、对比不同锁策略之间的应用场景及其区别1. 悲观锁 和 乐观锁1.1 定义和原理1.2 应用场景1.3 示例代码 2. 重量级锁 和 轻量级锁2.1 定义和原理2.2 应用场景2.3 示例代码 3. 挂起等待锁 和 自旋锁3.1 定义和原理3.2 应用场景3.3 示例代码 4. 几…...

软件测试 —— jmeter(2)

软件测试 —— jmeter&#xff08;2&#xff09; HTTP默认请求头&#xff08;元件&#xff09;元件作用域和取样器作用域HTTP Cookie管理器同步定时器jmeter插件梯度压测线程组&#xff08;Stepping Thread Group&#xff09;参数解析总结 Response Times over TimeActive Thre…...

[java] 面向对象进阶篇1--黑马程序员

目录 static 静态变量及其访问 实例变量及其访问 静态方法及其访问 实例方法及其访问 总结 继承 作用 定义格式 示例 总结 子类不能继承的内容 继承后的特点 成员变量 成员变量不重名 成员变量重名 super访问父类成员变量 成员方法 成员方法不重名 成员方法…...

openstack单机安装

openstack单机安装 网卡配置安装依赖开启虚拟环境修改配置文件 部署openstack部署openstack客户端访问可视化界面Horizon补充 本篇主要讲述Ubuntu2204单机安装openstackstable/2024.2。其他版本的Linux系统或者openstack版本&#xff0c;请参考openstack官网。 网卡配置 需要配…...

OFD、PDF 电子签章系统处理流程

在C#中实现电子签章系统的处理流程&#xff0c;可以参考以下步骤和技术实现&#xff1a; 1. 电子签章系统的基本流程 电子签章系统的核心流程包括以下几个步骤&#xff1a; 密钥生成&#xff1a;生成公钥和私钥对&#xff0c;私钥由签章人保管&#xff0c;公钥用于验证签名。…...

「 机器人 」系统辨识实验浅谈

前言 系统辨识实验是一种通过实验和数据分析的方法,用于建立物理系统的数学模型的技术。系统辨识是控制工程和系统科学中的重要环节,尤其是在模型未知或复杂的情况下。以下是系统辨识实验的详细介绍: 1. 系统辨识实验的目的 1.1 建模 为动态系统(如机械系统、电气系统或生…...

15.7k!DISM++一款快捷的系统优化工具

软件介绍 链接 软件介绍 dism是一款由初雨团队开发的win系统优化工具&#xff0c;可当作是微软系统命令行工具dism的GUI版本。可用作系统垃圾清除、启动项管理、程序卸载、驱动管理、系统优化等 该工具个人感觉最重要的就是系统优化选项&#xff0c;它将一些实用、无用或者没…...

Windows10安装MySQL找不到MSVCR120.dll和MSVCP120.dll问题解决

个人博客地址&#xff1a;Windows10安装MySQL找不到MSVCR120.dll和MSVCP120.dll问题解决 | 一张假钞的真实世界 msvcp120.dll、msvcr120.dll、vcomp120.dll属于VC2013版中的动态链接库&#xff0c;如果丢失重新安装VC2013即可。下载地址&#xff1a;https://www.microsoft.com…...

Vue 3 30天精进之旅:Day 03 - Vue实例

引言 在前两天的学习中&#xff0c;我们成功搭建了Vue.js的开发环境&#xff0c;并创建了我们的第一个Vue项目。今天&#xff0c;我们将深入了解Vue的核心概念之一——Vue实例。通过学习Vue实例&#xff0c;你将理解Vue的基础架构&#xff0c;掌握数据绑定、模板语法和指令的使…...

被遮挡QT窗口置顶

问题描述 开发环境&#xff1a;windows QT 需求&#xff1a; 单击托盘将桌面窗口在被遮挡的情况下置顶解决方案 方案1 资料链接 代码实现 Qt::WindowFlags flags windowFlags(); this->setWindowFlags((flags | Qt::WindowStaysOnTopHint)); this->showMaximized();…...

Apache Flink 概述学习笔记

一、引言 在大数据处理领域&#xff0c;Apache Flink 是一个极具影响力的开源流批一体化计算框架&#xff0c;它以其独特的架构和强大的功能&#xff0c;为大规模数据处理提供了高效、灵活的解决方案。 二、基本概念 Flink 是什么&#xff1a;Flink 是一个分布式流批处理框架…...

系统思考—复杂问题的根源分析

在企业中&#xff0c;许多问题看似简单&#xff0c;背后却潜藏着复杂的因果关系。传统的思维方式往往只能看到表面&#xff0c;而无法深入挖掘问题的真正根源。我们常常通过“表面解决”来应对眼前的症状&#xff0c;但这往往只是治标不治本。 比如&#xff0c;销量下降时&…...

Python 之 Excel 表格常用操作

示例文件 test.xlsx 将各个表单拆分成单独的 Excel 文件 import os.pathimport openpyxl import pandasdef handle_excel(file_path):dirname os.path.dirname(file_path)basename os.path.basename(file_path).split(".")[0]wb openpyxl.load_workbook(file_pat…...

《用DOTS解决实际需求》集锦

去年作者发布了一篇《DOTS-ECS系列课程》&#xff0c;深受同学们的好评&#xff01;前期课程是基于0.51版本录制的&#xff0c;DOTS升级至1.0版本后&#xff0c;同学们纷纷希望能使用DOTS 1.0版本录制实战课程。 今年作者带着DOTS 1.0版本的实战课程回来啦&#xff01;&#x…...

【MySQL】存储引擎有哪些?区别是什么?

频率难度60%⭐⭐⭐⭐ 这个问题其实难度并不是很大&#xff0c;只是涉及到的相关知识比较繁杂&#xff0c;比如事务、锁机制等等&#xff0c;都和存储引擎有关系。有时还会根据场景选择不同的存储引擎。 下面笔者将会根据几个部分尽可能地讲清楚 MySQL 中的存储引擎&#xff0…...

ios打包:uuid与udid

ios的uuid与udid混乱的网上信息 新人开发ios&#xff0c;发现uuid和udid在网上有很多帖子里是混淆的&#xff0c;比如百度下&#xff0c;就会说&#xff1a; 在iOS中使用UUID&#xff08;通用唯一识别码&#xff09;作为永久签名&#xff0c;通常是指生成一个唯一标识&#xf…...

Jadx动态调试安卓逆向

adb shell su ls 找到default.prop cat default.prop ro.debuggable0(代表没有调试权限) adb shell getprop ro.debuggable # 检查设备是否可调试&#xff08;1可调试&#xff09; adb shell getprop ro.product.cpu.abi # 获取设备 CPU 架构&#xff08;如 arm64-v…...

在Ubuntu上使用Apache+MariaDB安装部署Nextcloud并修改默认存储路径

一、前言 Nextcloud 是一款开源的私有云存储解决方案&#xff0c;允许用户轻松搭建自己的云服务。它不仅支持文件存储和共享&#xff0c;还提供了日历、联系人、任务管理、笔记等丰富的功能。本文将详细介绍如何在 Ubuntu 22.04 LTS 上使用 Apache 和 MariaDB 安装部署 Nextcl…...

FPGA实现任意角度视频旋转(二)视频90度/270度无裁剪旋转

本文主要介绍如何基于FPGA实现视频的90度/270度无裁剪旋转&#xff0c;关于视频180度实时旋转&#xff0c;请见本专栏前面的文章&#xff0c;旋转效果示意图如下&#xff1a; 为了实时对比旋转效果&#xff0c;采用分屏显示进行处理&#xff0c;左边代表旋转前的视频在屏幕中…...

六、深入了解DI

依赖注入是⼀个过程&#xff0c;是指IoC容器在创建Bean时,去提供运⾏时所依赖的资源&#xff0c;⽽资源指的就是对象. 在上⾯程序案例中&#xff0c;我们使⽤了 Autowired 这个注解&#xff0c;完成了依赖注⼊的操作. 简单来说,就是把对象取出来放到某个类的属性中。 关于依赖注…...

kotlin内联函数——let,run,apply,also,with的区别

一、概述 为了帮助您根据使用场景选择合适的作用域函数&#xff08;scope function&#xff09;&#xff0c;我们将对它们进行详细描述并提供使用建议。从技术上讲&#xff0c;许多情况下范围函数是可以互换使用的&#xff0c;因此示例中展示了使用它们的约定俗成的做法。 1.…...

火语言RPA—超级鹰打码

&#x1f6a9;【组件功能】&#xff1a;通过传入图像返回图像中的文字或结果信息 针对不同类型图片形式的验证码&#xff0c;提交至平台api&#xff0c;以字符串形式返回图片识别结果。 配置预览 配置说明 文件路径 支持T或# 默认FLOW输入项 待识别本地图片的完整路径。 用…...

C#新语法

目录 顶级语句&#xff08;C#9.0&#xff09; using 全局using指令&#xff08;C#10.0&#xff09; using资源管理问题 using声明&#xff08;C#8.0&#xff09; using声明陷阱 错误写法 正确写法 文件范围的命名空间声明&#xff08;C#10.0&#xff09; 可空引用类型…...

Cloudflare通过代理服务器绕过 CORS 限制:原理、实现场景解析

第一部分&#xff1a;问题背景 1.1 错误现象复现 // 浏览器控制台报错示例 Access to fetch at https://chat.qwenlm.ai/api/v1/files/ from origin https://ocr.doublefenzhuan.me has been blocked by CORS policy: Response to preflight request doesnt pass access con…...

lightgbm做分类

python import pandas as pd#导入csv文件的库 import numpy as np#进行矩阵运算的库 import json#用于读取和写入json数据格式#model lgb分类模型,日志评估,早停防止过拟合 from lightgbm import LGBMClassifier,log_evaluation,early_stopping #metric from sklearn.metrics …...

下载Visual Studio Community 2019

官方链接如下&#xff1a;Visual Studio Community 2019下载链接 https://learn.microsoft.com/zh-cn/visualstudio/releases/2019/system-requirements#download 目前官方仅建议2022版&#xff0c;已经关闭vs2019等旧版本&#xff0c;哪天开放了&#xff0c;记得踢我一下。 …...

深入理解MySQL事务(万字详)

文章目录 什么是事务为什么会出现事务事务的版本支持事务的提交方式事务常见操作方式正常演示 - 证明事务的开始与回滚非正常演示1 - 证明未commit&#xff0c;客户端崩溃&#xff0c;MySQL自动会回滚&#xff08;隔离级别设置为读未提交&#xff09;非正常演示2 - 证明commit了…...

FFPlay命令全集合

FFPlay是以FFmpeg框架为基础&#xff0c;外加渲染音视频的库libSDL构建的媒体文件播放器。 ffplay工具下载并播放视频&#xff0c;可以辅助卡看流信息。 官网下载地址&#xff1a;http://ffmpeg.org/download.html#build-windows 下载build好的exe程序&#xff1a; 此处下载…...

AI编程工具使用技巧:在Visual Studio Code中高效利用阿里云通义灵码

AI编程工具使用技巧&#xff1a;在Visual Studio Code中高效利用阿里云通义灵码 前言一、通义灵码介绍1.1 通义灵码简介1.2 主要功能1.3 版本选择1.4 支持环境 二、Visual Studio Code介绍1.1 VS Code简介1.2 主要特点 三、安装VsCode3.1下载VsCode3.2.安装VsCode3.3 打开VsCod…...

开源的Text-to-SQL工具WrenAI

WrenAI是一个开源的Text-to-SQL工具&#xff0c;旨在通过自然语言交互界面&#xff0c;帮助用户更便捷地查询数据库。以下是对WrenAI的详细介绍&#xff1a; 一、主要功能 自然语言交互&#xff1a;用户可以通过对话方式提出问题&#xff0c;WrenAI能够理解和解析复杂的查询需…...

python创建一个httpServer网页上传文件到httpServer

一、代码 1.server.py import os from http.server import SimpleHTTPRequestHandler, HTTPServer import cgi # 自定义请求处理类 class MyRequestHandler(SimpleHTTPRequestHandler):# 处理GET请求def do_GET(self):if self.path /:# 响应200状态码self.send_response(2…...

Linux中page、buffer_head、bio的关系

在Linux中&#xff0c;page、buffer_head、bio这三个概念紧密相关&#xff0c;共同构成了块设备I/O和内存管理的重要部分&#xff0c;它们的联系主要体现在以下方面&#xff1a; page与buffer_head 基于page构建&#xff1a;buffer_head通常是基于page来构建的&#xff0c;一…...

C++11新特性之decltype

1.decltype的作用 decltype是C11新增的一个关键字&#xff0c;与auto的功能一样&#xff0c;都是在编译期间推导变量类型的。不了解auto的可以转到——C11新特性之auto。 为什么引入decltype&#xff1f;看过上边那篇博客的读者应该知道auto在有些场景中并不适用,所以引入declt…...

对神经网络基础的理解

目录 一、《python神经网络编程》 二、一些粗浅的认识 1&#xff09; 神经网络也是一种拟合 2&#xff09;神经网络不是真的大脑 3&#xff09;网络构建需要反复迭代 三、数字图像识别的实现思路 1&#xff09;建立一个神经网络类 2&#xff09;权重更新的具体实现 3&am…...

后端开发Web

Maven Maven是apache旗下的一个开源项目&#xff0c;是一款用于管理和构建java项目的工具 Maven的作用 依赖管理 方便快捷的管理项目依赖的资源&#xff08;jar包&#xff09;&#xff0c;避免版本冲突问题 统一项目结构 提供标准、统一的项目结构 项目构建 标准跨平台(…...

QT 通过ODBC连接数据库的好方法:

效果图&#xff1a; PWD使用自己的&#xff0c;我的这是自己的&#xff0c;所以你用不了。 以下是格式。 // 1. 设置数据库连接 QSqlDatabase db QSqlDatabase::addDatabase("QODBC");// 建立和QMYSQL数据库的连接 // 设置数据库连接名称&#xff08;DSN&am…...

【Feature Scaling】:加速梯度下降法的利器

目录 特征缩放的目的常见的特征缩放方法1. 最小-最大缩放&#xff08;Min-Max Scaling&#xff09;2. 标准化&#xff08;Standardization 或 Z-Score Normalization&#xff09;3. 最大绝对值缩放&#xff08;Max Abs Scaling&#xff09; Rescale的使用场景结论 在机器学习中…...

QT:控件属性及常用控件(3)-----输入类控件(正则表达式)

输入类控件既可以进行显示&#xff0c;也能让用户输入一些内容&#xff01; 文章目录 1.Line Edit1.1 用户输入个人信息1.2 基于正则表达式的文本限制1.3 验证两次输入的密码是否一致1.4 让输入的密码可以被查看 2.Text Edit2.1 输入和显示同步2.1 其他信号出发情况 3.ComboBox…...

计算机网络 (59)无线个人区域网WPAN

前言 无线个人区域网&#xff08;WPAN&#xff0c;Wireless Personal Area Network&#xff09;是一种以个人为中心&#xff0c;采用无线连接方式的个人局域网。 一、定义与特点 定义&#xff1a;WPAN是以个人为中心&#xff0c;实现活动半径小、业务类型丰富、面向特定群体的无…...

Python Typing: 实战应用指南

文章目录 1. 什么是 Python Typing&#xff1f;2. 实战案例&#xff1a;构建一个用户管理系统2.1 项目描述2.2 代码实现 3. 类型检查工具&#xff1a;MyPy4. 常见的 typing 用法5. 总结 在 Python 中&#xff0c;静态类型检查越来越受到开发者的重视。typing 模块提供了一种方式…...

Redis存储③Redis基本命令+内部编号和架构

目录 1. Redis 命令行客户端 1.1 与 Redis 服务器交互 1.2 set 和 get 命令 2. 基本全局命令 2.1 keys 2.2 exists 2.3 del 2.4 expire 2.5 ttl 2.6 type 3. 数据结构和内部编码 4. 单线程架构 本篇完。 1. Redis 命令行客户端 1.1 与 Redis 服务器交互 根据上篇…...

Vivado生成X1或X4位宽mcs文件并固化到flash

1.生成mcs文件 01.在vivado里的菜单栏选择"tools"工具栏 02.在"tools"里选择"生成内存配置文件" 03.配置参数 按照FPGA板上的flash型号进行选型&#xff0c;相关配置步骤可参考下图。 注意&#xff1a;Flash数据传输位宽如果需要选择X4位宽&am…...

07 区块链安全技术

概述 区块链的安全特性 区块链解决了在不可靠网络上可靠地传输信息的难题&#xff0c;由于不依赖与中心节点的认证和管理&#xff0c;因此防止了中心节点被攻击造成的数据泄露和认证失败的风险。 区块链安全防护的三大特点 共识机制代替中心认证机制数据篡改“一发动全身”…...

第84期 | GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…...

柔性数组与c/c++程序中内存区域的划分

1.柔性数组 1.1柔性数组的定义 柔性数组是指在结构体中定义的&#xff0c;其大小在编译时未确定&#xff0c;而在运行时动态分配的数组。这种数组允许结构体的大小根据需要动态变化。语法如下&#xff1a; struct D {int a;int arry1[0]; };struct F {int a;int arry2[]; };…...

react页面定时器调用一组多个接口,如果接口请求返回令牌失效,清除定时器不再触发这一组请求

为了实现一个React页面使用定时器调用一组多个接口&#xff0c;并在任意一个接口请求返回令牌失效时清除定时器且不再触发这一组请求&#xff0c;可以遵循以下步骤&#xff1a; 1. 定义API调用函数&#xff1a;创建一个函数来处理一组API调用。每个API调用都应该检查响应状态以…...

使用 .NET Core 6.0 Web API 上传单个和多个文件

示例代码&#xff1a; https://download.csdn.net/download/hefeng_aspnet/90138968 介绍 我们将在 IFormFile 接口和 .NET 提供的其他接口的帮助下&#xff0c;逐步讨论单个和多个文件上传。 .NET 提供了一个 IFormFile 接口&#xff0c;代表 HTTP 请求中传输的文件。 此外…...

AJAX笔记入门篇

黑马程序员视频地址&#xff1a; 黑马程序员前端AJAX入门到实战全套教程https://www.bilibili.com/video/BV1MN411y7pw?vd_source0a2d366696f87e241adc64419bf12cab&spm_id_from333.788.videopod.episodes&p2 目录 AJAX 概念和axios 使用 什么是AJAX&#xff1f; …...

RoHS 简介

RoHS&#xff08;Restriction of Hazardous Substances Directive&#xff0c;限制有害物质指令&#xff09;是欧盟制定的一项环保法规&#xff0c;旨在限制电气和电子设备中某些有害物质的使用&#xff0c;以减少这些产品对环境和人体健康的危害。 RoHS限制的有害物质及其限量…...

C# 中 default 使用详解

总目录 前言 在C#中&#xff0c;default 关键字用于表示类型默认值。它可以根据上下文推断出适用的类型&#xff0c;并返回该类型的默认值。随着C#版本的发展&#xff0c;default 的用法也变得更加丰富和灵活。本文将详细介绍 default 在不同场景下的使用方法及其最佳实践。 一…...