欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > Java【多线程】(8)CAS与JUC组件

Java【多线程】(8)CAS与JUC组件

2025/4/17 6:17:20 来源:https://blog.csdn.net/2301_81073317/article/details/147224005  浏览:    关键词:Java【多线程】(8)CAS与JUC组件


目录

1.前言

2.正文

2.1CAS概念

2.2CAS两种用途

2.2.1实现原子类

2.2.2实现自旋锁

2.3缺陷:ABA问题

2.4JUC组件

2.4.1Callable接口

2.4.2ReentrantLock(与synchronized对比)

2.4.3Semaphore信号量

2.4.4CountDownLatch

3.小结


1.前言

哈喽大家好吖,不知不觉多线程这一块大骨头终于快要啃完了,今天给大家分享的是CAS以及JUC相关组件,那么废话不多说让我们开始吧。

2.正文

2.1CAS概念

核心思想:无所并发控制

CAS(Compare And Swap)是一种基于乐观锁的无锁并发控制技术。其核心逻辑可以概括为:“我认为当前值应该是A,如果是,则更新为B;否则放弃或重试”。整个过程由硬件保证原子性,无需传统锁机制。


通俗来说
假设你和同事协同编辑一份共享文档,每次保存时系统会检查:

  1. 当前内容是否和你打开时的版本一致(预期值比对)。

  2. 如果一致,允许保存;否则提示“内容已变更,请重新编辑”。
    这个过程就是CAS的核心思想——乐观锁:先操作,冲突时重试,而非直接加锁阻塞。


CAS操作的伪代码可以拆解为以下步骤,帮助理解其原子性本质:

// 伪代码:CAS操作的逻辑分解
public boolean compareAndSwap(MemoryAddress addr, int expectedValue, int newValue) {// 1. 读取内存当前值int currentValue = *addr; // 2. 比较当前值与预期值if (currentValue != expectedValue) {return false; // 值已被其他线程修改,操作失败}// 3. 若值未变,执行原子性更新*addr = newValue;return true;
}

2.2CAS两种用途

2.2.1实现原子类

针对原子类,++--这样的操作是原子的,基于CAS实现,不涉及到加锁。


传统实现:

private int count = 0;  
public synchronized void increment() {  count++;  
}  

进阶实现: (使用Java提供的原子类)

AtomicInteger count = new AtomicInteger(0);  
public void increment() {  int oldValue, newValue;  do {  oldValue = count.get();  newValue = oldValue + 1;  } while (!count.compareAndSet(oldValue, newValue)); // CAS自旋  
}  

2.2.2实现自旋锁

先回顾一个上篇文章的概念:自旋锁是线程通过循环(自旋)不断尝试获取锁,而非立即阻塞。适用于锁持有时间极短的场景。

代码实现:

public class CASSpinLock {  private AtomicBoolean locked = new AtomicBoolean(false);  // 获取锁  public void lock() {  while (!locked.compareAndSet(false, true)) {  // 自旋:直到成功将locked从false改为true  }  }  // 释放锁  public void unlock() {  locked.set(false);  }  
}  
  • 线程竞争不激烈时(如短任务),自旋锁比系统锁(如synchronized)更高效。

  • 缺点:长时间自旋会浪费CPU资源(需根据场景权衡)。

2.3缺陷:ABA问题

ABA问题场景

  1. 线程1读取变量值为A

  2. 线程2将值改为B,随后又改回A

  3. 线程1执行CAS操作,发现当前值仍是A,误认为未被修改过,操作成功。


通俗理解:

  • 你看到自己的水杯是满的(A),去接水时离开了一会儿。

  • 期间别人喝光水(A→B)又倒满(B→A)。

  • 你回来后以为水没被喝过,直接喝下(可能喝到别人的水!)。

这里在实际场景中就是非常严重的线程安全的问题了。

解决方案: 

 

1. 版本号标记(AtomicStampedReference)
为值附加一个版本号(类似“修改次数”),CAS时同时校验值和版本号。

AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);  // 线程1读取值和版本号  
int stamp = ref.getStamp();  
String oldValue = ref.getReference();  // 线程2修改值并更新版本号  
ref.compareAndSet("A", "B", stamp, stamp + 1);  
ref.compareAndSet("B", "A", stamp + 1, stamp + 2);  // 线程1尝试修改:虽然值还是A,但版本号已变,操作失败!  
boolean success = ref.compareAndSet(oldValue, "C", stamp, stamp + 1);  

2. 状态标记(AtomicMarkableReference)
用布尔值标记是否被修改过(简化版版本号)。

2.4JUC组件

2.4.1Callable接口

官方解析:Callable (Java SE 17 & JDK 17)

Callable 是 Java 并发包(JUC)中定义的接口,类似于 Runnable,但允许线程执行任务后返回结果,并可以抛出异常。

与 Runnable 的区别:

  • Runnable 的 run() 没有返回值,Callable 的 call() 可以返回泛型结果。

  • call() 可以抛出受检异常,run() 不能。

具体案例(异步运算1加到100):

Callable<Integer> task = () -> {int sum = 0;for (int i = 1; i <= 100; i++) sum += i;return sum;
};
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();// 主线程获取结果
System.out.println("计算结果:" + futureTask.get()); // 输出 5050

通过 FutureTask 包装 Callable 任务,启动线程执行后,主线程通过 futureTask.get() 等待结果返回,类似“异步任务+回调”模式。 

2.4.2ReentrantLock(与synchronized对比)

官方解析:ReentrantLock (Java SE 17 & JDK 17)

ReentrantLock 是 JUC 提供的显式锁,支持可重入性、可中断锁、公平锁等特性。

特性synchronizedReentrantLock
锁获取方式隐式(JVM 管理)显式(代码手动加锁/解锁)
可中断不支持支持 lockInterruptibly()
公平锁不支持支持(构造函数指定)
条件变量(Condition)支持(newCondition()

 案例:

class BankAccount {private final ReentrantLock lock = new ReentrantLock();private int balance = 100;void transfer(BankAccount target, int amount) {lock.lock();try {if (this.balance >= amount) {this.balance -= amount;target.balance += amount;}} finally {lock.unlock(); // 必须手动释放锁}}
}

synchronized 的等价实现是在方法签名加 synchronized 关键字,但 ReentrantLock 更灵活:

  • 可设置超时时间(tryLock(1, TimeUnit.SECONDS))。

  • 公平锁减少线程饥饿问题。

2.4.3Semaphore信号量

官方解析:Semaphore (Java SE 17 & JDK 17)

Semaphore 用于控制同时访问某个资源的线程数量,类似“许可证发放”。

核心方法

  • acquire():获取许可证(若无可用则阻塞)。

  • release():释放许可证。

 案例:(模拟停车场)

Semaphore semaphore = new Semaphore(3); // 3 个许可证Runnable parkAction = () -> {try {semaphore.acquire(); // 获取车位System.out.println(Thread.currentThread().getName() + " 停入车位");Thread.sleep(2000); // 停车 2 秒} catch (InterruptedException e) {e.printStackTrace();} finally {semaphore.release(); // 释放车位System.out.println(Thread.currentThread().getName() + " 离开车位");}
};// 启动 5 辆车尝试停车
for (int i = 0; i < 5; i++) {new Thread(parkAction).start();
}

2.4.4CountDownLatch

官方解析:CountDownLatch (Java SE 17 & JDK 17)

CountDownLatch 是一个同步工具,允许一个或多个线程等待其他线程完成操作。

核心方法:

  • countDown():计数器减 1。

  • await():阻塞直到计数器归零。 

public static void main(String[] args) {CountDownLatch latch = new CountDownLatch(3); // 需要等待 3 个任务// 资源加载任务Runnable loadTask = () -> {try {Thread.sleep((long) (Math.random() * 2000));System.out.println(Thread.currentThread().getName() + " 加载完成");latch.countDown();} catch (InterruptedException e) {e.printStackTrace();}};// 启动 3 个资源加载线程new Thread(loadTask, "地图").start();new Thread(loadTask, "音效").start();new Thread(loadTask, "UI").start();// 主线程等待所有资源加载完成try {latch.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("所有资源加载完成,开始游戏!");}

3.小结

今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,你的支持就是对我最大的鼓励,大家加油!

 

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词