Java CAS与JUC组件详解
作者:爱吃烤鸡翅的酸菜鱼
1.前言
哈喽大家好吖,不知不觉多线程这一块大骨头终于快要啃完了,今天给大家分享的是CAS以及JUC相关组件,那么废话不多说让我们开始吧。
2.正文
2.1CAS概念
核心思想:无所并发控制
CAS(Compare And Swap)是一种基于乐观锁的无锁并发控制技术。其核心逻辑可以概括为:“我认为当前值应该是A,如果是,则更新为B;否则放弃或重试”。整个过程由硬件保证原子性,无需传统锁机制。
通俗来说
假设你和同事协同编辑一份共享文档,每次保存时系统会检查:当前内容是否和你打开时的版本一致(预期值比对)。
如果一致,允许保存;否则提示“内容已变更,请重新编辑”。
这个过程就是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读取变量值为
A。 - 线程2将值改为
B,随后又改回A。 - 线程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 提供的显式锁,支持可重入性、可中断锁、公平锁等特性。
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁获取方式 | 隐式(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.小结
今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,你的支持就是对我最大的鼓励,大家加油!
到此这篇关于Java CAS与JUC组件的文章就介绍到这了,更多相关Java CAS与JUC组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
