目录
2.4.2ReentrantLock(与synchronized对比)
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.小结
今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,你的支持就是对我最大的鼓励,大家加油!
平台声明:以上文章转载于《CSDN》,文章全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,仅作参考。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/2301_81073317/article/details/147224005