Java 多线程进阶:锁策略、CAS 与 JUC 组件详解
Java 多线程进阶涵盖常见锁策略对比,包括悲观乐观、重量级轻量级、挂起自旋、读写互斥、可重入及公平性分析。重点解析 synchronized 锁升级机制(偏向锁至重量级锁)及优化措施如锁消除与粗化。深入探讨 CAS 原理、应用场景及 ABA 问题解决方案。最后介绍 JUC 核心组件 Callable、ReentrantLock、Semaphore 和 CountDownLatch 的使用与区别。

Java 多线程进阶涵盖常见锁策略对比,包括悲观乐观、重量级轻量级、挂起自旋、读写互斥、可重入及公平性分析。重点解析 synchronized 锁升级机制(偏向锁至重量级锁)及优化措施如锁消除与粗化。深入探讨 CAS 原理、应用场景及 ABA 问题解决方案。最后介绍 JUC 核心组件 Callable、ReentrantLock、Semaphore 和 CountDownLatch 的使用与区别。

悲观和乐观是指锁竞争的激烈情况。
这两种锁就对应上述的悲观乐观情况下的处理机制。
这又是对上面的重量级与轻量级锁的典型实现。
这两种锁是针对加锁解锁时的线程安全问题。
这组锁就是针对同一个线程多次嵌套获取同一把锁。
这组锁是针对线程获取锁的概率设置的。
锁策略如下:
synchronized 的自适应过程称为锁升级:无锁 -> 偏向锁 -> 自旋锁 -> 重量级锁。
除了上面的 synchronized 锁升级以外,还有以下的优化措施:
锁粗化:一个代码对细粒度的代码反复加锁解锁,就会将这个步骤优化为更粗粒度的加锁解锁。

CAS: 全称 Compare and swap,字面意思:'比较并交换',一个 CAS 涉及到以下操作:
我们假设内存中的原数据 V,旧的预期值 A,需要修改的新值 B。比较 A 与 V 是否相等。(比较)如果比较相等,将 B 写入 V。(交换)返回操作是否成功。
伪代码表示如下:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
原子类:特指 java.util.concurrent.atomic 包下的类,这些类中的操作都是原子的。
例如执行下面这段代码:结果就一直是 100000。
import java.util.concurrent.atomic.AtomicInteger;
public class Demo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.incrementAndGet();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.incrementAndGet();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count.get());
}
}
原子类线程安全的原理:
假设两个线程同时调用 getAndIncrement:两个线程都读取 value 的值到 oldValue 中。(oldValue 是一个局部变量,在栈上,每个线程有自己的栈) 线程 1 先执行 CAS 操作。由于 oldValue 和 value 的值相同,直接进行对 value 赋值。 线程 2 再执行 CAS 操作,第一次 CAS 的时候发现 oldValue 和 value 不相等,不能进行赋值。因此需要进入循环。在循环里重新读取 value 的值赋给 oldValue。 线程 2 接下来第二次执行 CAS,此时 oldValue 和 value 相同,于是直接执行赋值操作。
线程 1 和 线程 2 返回各自的 oldValue 的值即可。

基于 CAS 实现更灵活的自旋锁,获取到更多的控制权。
主要逻辑如下:
伪代码描述如下:
public class SpinLock {
private Thread owner = null;
public void lock() {
while (!CAS(this.owner, null, Thread.currentThread())) {
}
}
public void unlock() {
this.owner = null;
}
}
假设存在两个线程 t1 和 t2。有一个共享变量 num,初始值为 A。 接下来,线程 t1 想使用 CAS 把 num 值改成 Z,那么就需要先读取 num 的值,记录到 oldNum 变量中。 使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改成 Z。 但是,在 t1 执行这两个操作之间,t2 线程可能把 num 的值从 A 改成了 B,又从 B 改成了 A。 线程 t1 的 CAS 是期望 num 不变就修改。但是 num 的值已经被 t2 给改了。只不过又改成 A 了。这 个时候 t1 究竟是否要更新 num 的值为 Z。就不符合预期出现 Bug。
过程图:
Bug 例子:
假如你去取钱有 1000,取 500,不小心按了两次取款,我们只想扣一次 500,第一次按相当于 T2,第二次按相当于 T1,先执行 T2 读取操作修改为 500,这时又正好有人给你汇款了 500,内存中与寄存器中又相等了,T1 扣款的操作也会执行成功。
造成 ABA 问题的原因就是内存值既有加又有减,所以我们的解决 ABA 的方式如下。
给要修改的值,引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。 CAS 操作在读取旧值的同时,也要读取版本号。 真正修改的时候:
JUC 组件就是指 java.util.concurrent 这个包下面的类。
Callable 接口就和 java.lang 包下的 Runnable 接口的定位差不多,只不过给了我们一个返回值,在 Runnable 接口要重写 run 方法,而在 Callable 接口中要重写 call 方法。

在使用 Callable 接口传入线程时需要借助 JUC 下的另一个类 FutureTask。

使用例子一般如下:
public class Demo {
public static void main(String[] args) {
//创建实现了 Callable 接口的方法
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return null;
}
};
//将实现 Callable 接口的对象作为参数传入 FutureTask,泛型参数要一致
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//将 FutureTask 对象作为参数传入 Thread
Thread thread = new Thread(futureTask);
}
}
ReentrantLock 类可重入互斥锁,和 synchronized 的定位是差不多的。

ReentrantLock 的用法:
ReentrantLock locker = new ReentrantLock();
try {
locker.lock();
} finally {
locker.unlock();
}
ReentrantLock 与 synchronized 的区别:
ReentrantLock 还提供了公平锁的实现,默认是非公平锁,但是构造方法传参 true 实现公平锁。

信号量,:用来表示'可用资源的个数'。本质上就是一个计数器,能够协调多个线程之间的资源调配。 最主要的就是申请资源(P 操作,调用 acquire 方法)释放资源(V 操作,调用 release 方法)
可以把信号量想象成是停车场的展示牌:当前有车位 100 个。表示有 100 个可用资源。当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1 (这个称为信号量的 P 操作) 当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1 (这个称为信号量的 V 操作) 如果计数器的值已经为 0 了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源.

CountDownLatch 类的作用:同时等待 N 个任务执行结束。(就像跑步比赛,10 个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。)
使用多线程的时候,经常会把一个大任务拆分为多个子任务,使用 CountDownLatch 衡量子任务完成,让整个任务完成。
主线程中使用 await() 方法; 阻塞等待所有任务执行完毕后 wait 结束。相当于计数器为 0 了。


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online