一、CAS
CAS 是乐观锁最核心、最底层的实现机制,全称是 Compare And Swap(比较并交换),它是一条 CPU 级别的原子指令,能保证操作的原子性,也是很多无锁编程的基础。
核心逻辑
CAS 操作包含三个核心参数:
- 内存地址(V):要操作的共享变量在内存中的位置;
- 预期值(A):线程读取到的共享变量的原始值;
- 新值(B):线程想要把共享变量更新成的目标值。
它的执行逻辑可以通俗理解为:
线程在更新共享变量前,先去内存地址 V 处查当前值。如果这个值和我之前读到的预期值 A 完全一样,说明没人改过,我就把它换成新值 B;如果不一样,说明已经被其他线程改了,这次更新就失败,我不做任何操作。
整个过程是原子性的——CPU 会保证这个“比较+交换”的动作要么全做完,要么全不做,不会被其他线程打断,这也是它能替代锁的关键。
执行流程
举个生活中的例子:你想给手机充话费(共享变量是话费余额):
- 你先查了下余额,显示还有 10 元(这就是「预期值 A」);
- 你准备充 50 元,想把余额改成 60 元(这就是「新值 B」);
- 你提交充值操作时,系统会先去查当前余额(「内存地址 V」的实际值):
- 如果实际余额还是 10 元,说明没人中途操作,就把余额改成 60 元,充值成功;
- 如果实际余额已经不是 10 元(比如家人先给你充了 100 元,余额变成 110),说明数据被改了,充值操作失败,你可以选择重新查余额再试,或者放弃。
特点与应用
核心特点
- 无锁:不需要像悲观锁那样阻塞线程,线程更新失败时只是重试或放弃,开销远小于锁;
- 原子性:由 CPU 指令保证,无需额外的同步手段;
- 非阻塞:即使更新失败,当前线程也不会被挂起,能继续执行其他逻辑。
典型应用
- 编程语言层面:Java 中的
java.util.concurrent.atomic包(比如AtomicInteger、AtomicLong)就是基于 CAS 实现的,比如AtomicInteger.incrementAndGet()(自增)就是通过 CAS 完成,替代了加锁的i++; - 数据库层面:乐观锁的版本号更新(
update ... where version = ?)本质上是 CAS 思想的体现; - 分布式系统:Redis 的
SETNX命令、分布式锁的实现也用到了 CAS 思想。
局限性
- ABA 问题:这是最典型的问题——如果变量值从 A 被改成 B,又改回 A,CAS 会认为值没变化,但实际上已经被修改过。解决办法是给变量加版本号/时间戳(比如 Java 的
AtomicStampedReference),不仅比较值,还比较版本; - 自旋开销:如果高并发下更新频繁失败,线程会不断重试(自旋),会消耗 CPU 资源;
- 只能操作单个变量:CAS 只能保证单个变量操作的原子性,如果要操作多个变量,还是需要借助锁或其他方式(比如把多个变量封装成一个对象)。
二、多种锁
悲观锁
核心思想
悲观地认为并发操作一定会发生冲突,因此在整个数据处理过程中,会强制锁定资源,阻止其他线程对资源的访问,直到当前线程操作完成。
可以理解为:“凡事往最坏的方向想,先锁住资源,再干活,避免别人捣乱”。
实现方式
- 数据库层面:使用
SELECT ... FOR UPDATE语句,查询数据时直接锁定对应的行,其他事务无法对其进行修改或加锁,直到当前事务提交或回滚。 - Java层面:使用
synchronized关键字,或者ReentrantLock等独占锁,同一时刻只有一个线程能执行被锁定的代码块。
适用场景
- 并发冲突概率高的场景,比如库存扣减、转账等核心业务逻辑。
- 对数据一致性要求极高的场景,不允许出现脏读、不可重复读等问题。
优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,数据一致性强 | 性能开销大,容易出现死锁 |
| 冲突处理直接,无需重试 | 会阻塞其他线程,并发效率低 |
乐观锁
核心思想
乐观地认为并发操作大概率不会发生冲突,因此不会主动锁定资源,而是在更新数据时,检查数据是否被其他线程修改过。如果未被修改,则正常更新;如果已被修改,则放弃当前操作或重试。
可以理解为:“凡事往最好的方向想,先干活,最后再检查有没有人动过数据”。
实现方式
-
版本号机制
- 数据库表中增加一个
version字段,初始值为0。 - 读取数据时,同时读取
version的值。 - 更新数据时,执行
UPDATE ... WHERE id = ? AND version = ?,并将version加1。 - 检查更新影响的行数,如果为
0,说明数据已被其他线程修改,执行重试或返回失败。
- 数据库表中增加一个
-
CAS 机制
- 是一种无锁算法,包含三个核心参数:
内存地址V、预期值A、新值B。 - 原理:只有当内存地址
V中的值等于预期值A时,才会将其更新为B,这个操作是原子性的;如果不相等,则说明值已被修改,放弃更新或重试。 - Java中的
AtomicInteger、AtomicReference等原子类就是基于CAS实现的。
- 是一种无锁算法,包含三个核心参数:
适用场景
- 并发冲突概率低的场景,比如用户资料修改、文章点赞等非核心业务。
- 追求高并发性能,可以接受少量冲突重试的场景。
优缺点
| 优点 | 缺点 |
|---|---|
| 不阻塞线程,并发效率高 | 冲突时需要重试,增加业务复杂度 |
| 没有锁的开销,性能好 | 存在ABA问题(数据被修改后又改回原值,CAS会误判未修改) |
| 高冲突场景下,重试次数过多会导致性能下降 |
说明:重量级锁和轻量级锁是 Java 中
synchronized关键字在不同并发场景下的两种锁实现形态,核心区别在于是否依赖操作系统的互斥量,以及对性能开销的影响。二者的设计目的是为了在不同并发压力下,让synchronized达到最优性能——这也是 JDK 1.6 对synchronized做的核心优化(之前的synchronized只有重量级锁)。
轻量级锁
核心思想
基于“大多数情况下,锁不会存在多线程竞争”的假设,采用无锁的 CAS 操作来实现加锁/解锁,避免调用操作系统的内核态函数,从而降低性能开销。
实现原理
- 加锁阶段
- 当线程进入同步代码块时,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record)对象,用于存储锁对象的 Mark Word(对象头中的标记字段)的拷贝。
- 线程通过 CAS 操作,将锁对象的 Mark Word 更新为指向自身锁记录的指针。
- 如果 CAS 成功,线程就持有了该轻量级锁;如果失败(说明有其他线程竞争),则膨胀为重量级锁。
- 解锁阶段
- 线程退出同步代码块时,通过 CAS 操作将锁对象的 Mark Word 恢复为原始值。
- 如果 CAS 成功,解锁完成;如果失败,说明锁已经膨胀,需要唤醒被阻塞的线程。
适用场景
- 多线程交替执行同步代码块的场景(几乎没有锁竞争)。
- 锁持有时间很短的场景。
优缺点
| 优点 | 缺点 |
|---|---|
| 基于用户态 CAS 操作,开销小 | 一旦出现锁竞争,会膨胀为重量级锁,产生额外开销 |
| 不会阻塞线程 | 竞争激烈时,性能不如重量级锁 |
重量级锁
核心思想
基于“存在多线程竞争”的假设,依赖操作系统的互斥量实现加锁,会导致线程在用户态和内核态之间切换,性能开销较大。
实现原理
- 加锁阶段
- 当轻量级锁的 CAS 操作失败(出现竞争),JVM 会将锁升级为重量级锁。
- 锁对象的 Mark Word 会被更新为指向一个互斥量的指针。
- 竞争锁失败的线程会被阻塞,并放弃 CPU 执行权,进入阻塞队列。
- 解锁阶段
- 持有锁的线程释放锁时,会唤醒阻塞队列中的线程,这些线程会重新竞争锁。
适用场景
- 多线程同时竞争锁的场景(高并发冲突)。
- 锁持有时间较长的场景(比如同步代码块内有 IO 操作、sleep 等)。
优缺点
| 优点 | 缺点 |
|---|---|
| 适合高竞争场景,稳定性强 | 线程阻塞/唤醒需要切换内核态,开销大 |
| 不会产生大量 CAS 重试消耗 | 存在线程上下文切换的性能损耗 |
自旋锁
核心思想
自旋锁是一种非阻塞的轻量级锁机制,核心思想是:当线程竞争锁失败时,不立即进入阻塞状态,而是通过循环(自旋)不断尝试获取锁,直到成功获取锁,或者达到预设的自旋次数上限后再放弃。
核心设计逻辑
- 基于假设:锁被持有的时间很短,线程通过短暂自旋就能拿到锁,这样可以避免线程从用户态切换到内核态的巨大开销(线程阻塞/唤醒需要内核态操作)。
- 本质:用 CPU 时间换取线程切换的开销,适合锁持有时间短、并发冲突不激烈的场景。
实现原理
在 Java 中,自旋锁是 synchronized 锁升级流程中的一个环节,具体过程如下:
- 当轻量级锁的 CAS 操作失败(出现线程竞争),JVM 不会直接将锁升级为重量级锁,而是先触发自旋。
- 竞争锁失败的线程会进入一个循环,不断执行 CAS 操作尝试获取锁。
- 自旋过程中,线程处于 Runnable 状态,不会释放 CPU 资源。
- 存在两种结果:
- 自旋成功:在自旋次数上限内拿到锁,继续执行同步代码块。
- 自旋失败:达到自旋次数上限仍未拿到锁,此时锁会膨胀为重量级锁,该线程会被挂起,进入阻塞队列。
关键特性
| 特性 | 说明 |
|---|---|
| 非阻塞 | 竞争失败时不阻塞,而是自旋尝试 |
| CPU 消耗 | 自旋时会占用 CPU 资源,若锁持有时间长,会造成 CPU 空转浪费 |
| 自旋次数 | JDK 中默认自旋次数是10 次,也可以通过参数 -XX:PreBlockSpin 手动设置 |
| 自适应自旋 | JDK 1.6 引入自适应自旋,自旋次数不再固定,而是根据前一次获取该锁的自旋时间和锁的拥有者状态动态调整(比如之前自旋 5 次成功,这次也大概率自旋 5 次) |
适用场景 vs 不适用场景
| 适用场景 | 不适用场景 |
|---|---|
| 锁持有时间极短(如几纳秒/微秒) | 锁持有时间长(如包含 IO 操作、sleep) |
| 并发线程数不多 | 并发线程数远大于 CPU 核心数(会导致大量线程自旋,CPU 被耗尽) |
| 多核 CPU 环境(有多余核心支持自旋) | 单核 CPU 环境(自旋会占用唯一的 CPU 资源,导致其他线程无法执行) |
与其他锁的关系
- 和轻量级锁的关系:自旋锁是轻量级锁竞争时的补充策略,用于延缓锁升级为重量级锁的时机。
- 和重量级锁的关系:自旋失败后,锁会升级为重量级锁,线程从自旋转为阻塞。
- 和乐观锁的关系:二者都基于 CAS 操作,但乐观锁是无锁的思想(不锁定资源,只在更新时检查),自旋锁是有锁的思想(必须拿到锁才能执行)。
公平锁
核心思想
严格遵循FIFO(先进先出)原则:所有竞争锁的线程会按请求锁的先后顺序排队,只有队列头部的线程能获取锁,新请求锁的线程必须排到队列末尾等待,不允许「插队」。
可以理解为:“排队买票,先来的先买,后到的必须排最后,绝对不允许加塞”。
实现原理
- 当线程请求锁时,首先检查锁是否空闲:
- 若锁空闲,且等待队列为空,则直接获取锁;
- 若锁空闲,但等待队列有线程,则当前线程加入队列尾部等待。
- 当持有锁的线程释放锁时,会唤醒等待队列头部的线程,让其获取锁。
优缺点
| 优点 | 缺点 |
|---|---|
| 绝对公平,每个线程都能按顺序获取锁,不会出现「线程饥饿」(某个线程一直拿不到锁) | 性能开销大:线程切换和队列维护会增加额外开销 |
| 避免长时间等待的线程一直抢不到锁 | 吞吐量低:即使锁刚释放且当前线程正好请求,也必须排队,无法利用「空窗期」 |
适用场景
- 对公平性要求极高的场景(如金融交易、任务调度);
- 线程持有锁时间较长,或线程请求锁的频率较低的场景(此时公平性带来的性能损耗可接受)。
非公平锁
核心思想
不遵循严格的排队顺序:线程请求锁时,会先尝试直接抢占锁(不排队),只有抢占失败时,才会加入等待队列尾部。即使等待队列中有线程在排队,新请求的线程也可能「插队」成功。
可以理解为:“买票时,先冲上去试试窗口有没有空位,有空位就直接买,没空位再排队;甚至有人刚买完票,你正好冲上去,就不用排队了”。
实现原理
- 当线程请求锁时,首先尝试通过 CAS 操作直接获取锁(无视等待队列);
- 若抢占成功,直接持有锁;若抢占失败,再加入等待队列尾部等待;
- 当持有锁的线程释放锁时,唤醒队列头部线程的同时,新请求锁的线程仍可能抢占,导致被唤醒的线程再次竞争失败。
优缺点
| 优点 | 缺点 |
|---|---|
| 性能高、吞吐量大:减少线程切换和队列调度开销,能利用锁释放的「空窗期」 | 不公平:可能出现「线程饥饿」,某些线程长时间排队却一直被新线程插队抢锁 |
是大多数场景的默认选择(如 ReentrantLock 无参构造、synchronized) | 极端情况下,队列尾部线程可能长期无法获取锁 |
适用场景
- 对性能要求高于公平性的场景(如普通业务逻辑、高并发接口);
- 线程持有锁时间极短,或线程请求锁频率高的场景(此时抢占的收益远大于公平性损耗)。
读写锁
核心思想
读写锁是一种适用于读多写少场景的共享-独占锁,核心设计目标是在保证数据一致性的前提下,提升并发读取的效率。它将对资源的访问分为读操作和写操作两种类型,分别对应不同的锁规则。
读写锁遵循“多读单写”原则:
- 读锁(共享锁):多个线程可以同时持有读锁,并发读取资源,互不阻塞。
- 写锁(独占锁):只有一个线程能持有写锁,写锁会排斥所有读锁和其他写锁,即写操作与读操作、写操作与写操作之间都是互斥的。
简单理解:读和读可以共存,读和写、写和写互斥。
实现原理
Java 中的 ReentrantReadWriteLock 是读写锁的典型实现,它内部维护了两把锁:
- 读锁(
ReadLock)- 当线程请求读锁时,若当前没有线程持有写锁,且没有线程在等待写锁(非公平模式),则直接获取读锁;
- 读锁的持有数量会被计数,一个线程可重复获取读锁(可重入),释放次数需与获取次数一致。
- 写锁(
WriteLock)- 当线程请求写锁时,必须满足没有任何线程持有读锁或写锁,才能成功获取;
- 写锁也是可重入的,持有写锁的线程可以再获取读锁(降级),但持有读锁的线程不能直接获取写锁(升级)。
核心特性
-
锁的互斥规则
操作场景 是否互斥 读锁 vs 读锁 不互斥(并发读取) 读锁 vs 写锁 互斥(读时不能写,写时不能读) 写锁 vs 写锁 互斥(同一时间只能一个写操作) -
可重入性
- 读锁可重入:一个线程获取读锁后,可再次获取读锁,释放时需对应次数。
- 写锁可重入:一个线程获取写锁后,可再次获取写锁,也可获取读锁(锁降级)。
-
锁降级
指写锁降级为读锁的过程,步骤为:- 线程持有写锁;
- 线程获取读锁;
- 线程释放写锁;
最终线程持有读锁。
注意:不支持锁升级(读锁不能直接升级为写锁),否则会导致死锁。
三、synchronized 原理
synchronized 是乐观锁也是悲观锁,是轻量级锁也是重量级锁,是挂起等待锁也是自旋锁,是不公平锁、可重入锁,不是读写锁。
加锁的工作过程
synchronized 是 Java 中最基础的互斥同步锁,JDK 1.6 对其做了重大优化(引入了偏向锁、轻量级锁、重量级锁),核心是从无锁到偏向锁,再到轻量级锁,最后升级为重量级锁的“锁升级”过程,目的是减少重量级锁带来的性能开销。
偏向锁
- 适用场景:单线程重复获取同一把锁(最常见的场景)。
- 加锁过程:
- 线程第一次获取锁时,JVM 会修改对象头的 Mark Word:将偏向锁标识设为 1,记录当前线程的 ID,无需 CAS 操作,开销极低。
- 后续该线程再次获取锁时,只需检查 Mark Word 中的线程 ID 是否是自己,无需任何同步操作,直接获取锁。
- 解锁过程:偏向锁不会主动释放,只有当其他线程尝试竞争锁时,才会触发偏向锁的撤销(需要暂停持有锁的线程,将偏向锁升级为轻量级锁)。
轻量级锁
- 适用场景:多个线程交替获取锁(无激烈竞争)。
- 加锁过程:
- 线程在自己的栈帧中创建一个
Lock Record(锁记录),存储对象头 Mark Word 的拷贝。 - 通过 CAS 操作将对象头的 Mark Word 替换为指向当前线程 Lock Record 的指针。
- CAS 成功则获取锁;失败则检查对象头是否指向自己的 Lock Record(重入),是则直接获取,否则升级为重量级锁。
- 线程在自己的栈帧中创建一个
- 解锁过程:
- 通过 CAS 将对象头的 Mark Word 恢复为原始值。
- CAS 成功则解锁完成;失败则说明有其他线程竞争,需唤醒等待线程。
重量级锁
- 适用场景:多个线程同时竞争锁(激烈竞争)。
- 加锁过程:
- JVM 为对象创建一个
Monitor(监视器,底层依赖操作系统的互斥量 Mutex)。 - 线程获取锁时,会进入 Monitor 的等待队列,若获取失败则被操作系统挂起(从用户态切换到内核态,开销极大)。
- JVM 为对象创建一个
- 解锁过程:
- 线程释放锁时,唤醒 Monitor 等待队列中的线程,让其竞争锁。
关键优化
JVM 的即时编译器(JIT)会对 synchronized 做运行时优化,减少不必要的锁开销,核心就是锁消除和锁粗化。
锁消除
- 核心含义:JIT 检测到某些锁对象是“局部私有”的,不存在多线程竞争的可能,直接消除该锁,避免加解锁的开销。
- 判断依据:基于“逃逸分析”——分析对象的作用域是否逃出当前线程/方法。
- 示例代码:
public String concat(String s1, String s2) {
// StringBuilder 是方法内局部变量,不会逃逸到方法外,无多线程竞争
StringBuilder sb = new StringBuilder();
sb.append(s1); // append 方法内部有 synchronized 锁
sb.append(s2);
return sb.toString();
}
- 优化过程:
JIT 会发现sb是局部变量,只有当前线程能访问,因此会消除append方法内的synchronized锁,执行时无需加解锁。 - 典型场景:局部变量的同步、不可变对象的同步(如 String)、单线程访问的集合同步等。
锁粗化
- 核心含义:JIT 检测到多个连续的加解锁操作针对同一个锁对象,会将这些分散的锁操作合并为一个“粗粒度”的锁,减少加解锁的次数。
- 核心目的:避免频繁加解锁带来的开销(即使是轻量级锁,CAS 也有少量开销)。
- 示例代码(优化前):
public void loopAdd() {
Object lock = new Object();
for (int i = 0; i < 1000; i++) {
// 循环内频繁加解锁,共 1000 次
synchronized (lock) {
System.out.println(i);
}
}
}
- 优化后(JIT 自动处理):
public void loopAdd() {
Object lock = new Object();
// 锁粗化:合并为 1 次加解锁
synchronized (lock) {
for (int i = 0; i < 1000; i++) {
System.out.println(i);
}
}
}
- 典型场景:循环内的同步操作、连续多次调用带同步的方法(如 StringBuilder 的多次 append)。
四、JUC 常见类
Callable 接口
Callable 与 Runnable 类似,都是用于定义线程任务的接口,但 Callable 中的 call() 方法可以自定义返回值,还能抛出受检异常,弥补了 Runnable 无返回值的缺陷。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 使用 Callable 定义任务,返回值为 Integer 类型
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
return sum;
}
};
// 通过 FutureTask 包装 Callable,用于接收返回结果
FutureTask<Integer> task = new FutureTask<>(callable);
Thread t = new Thread(task);
t.start();
// get() 方法阻塞等待任务执行完成,获取返回结果
System.out.println(task.get());
}
}
ReentrantLock 可重入锁
ReentrantLock 是 Java 并发包提供的可重入独占锁,通过 lock() 方法加锁、unlock() 方法解锁,相比 synchronized 提供了更灵活的锁控制能力。
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 创建 ReentrantLock 实例(默认非公平锁,传入 true 为公平锁)
ReentrantLock locker = new ReentrantLock();
Thread t1 = new Thread(()-> {
for (int i = 0; i < 100; i++) {
// 搭配 finally 使用,确保锁一定会被释放,避免死锁
try {
locker.lock(); // 加锁
count++;
} finally {
locker.unlock(); // 解锁
}
}
});
Thread t2 = new Thread(()-> {
for (int i = 0; i < 100; i++) {
try {
locker.lock(); // 加锁
count++;
} finally {
locker.unlock(); // 解锁
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
synchronized 和 ReentrantLock 的区别
- 使用方式:
synchronized搭配代码块使用,自动加解锁;ReentrantLock调用lock()/unlock()手动加解锁,需配合finally保证解锁。 - 实现层面:
synchronized是 Java 关键字,底层由 JVM 通过 C++ 实现;ReentrantLock是并发包的类,核心逻辑由 Java 实现(底层依赖系统 API)。 - 额外功能:
ReentrantLock提供tryLock()方法(支持超时获取锁、立即返回获取结果),还支持公平锁实现。 - 条件变量:
ReentrantLock可通过newCondition()获取多个条件变量,灵活实现线程间通信;synchronized仅依赖wait()/notify(),功能单一。
Semaphore 信号量
信号量本质是一个计数器,用来描述“可用资源的个数”,核心用于控制同时访问某个资源的线程数量,Java 并发包对系统原生信号量进行了封装。
import java.util.concurrent.Semaphore;
public class Demo {
public static void main(String[] args) throws InterruptedException {
// 初始化信号量,计数器初始值为 3(表示有 3 个可用资源)
Semaphore semaphore = new Semaphore(3);
semaphore.acquire(); // 申请资源,计数器减 1
System.out.println("申请资源1");
semaphore.acquire(); // 申请资源,计数器减 1
System.out.println("申请资源2");
semaphore.acquire(); // 申请资源,计数器减 1
System.out.println("申请资源3");
semaphore.release(); // 释放资源,计数器加 1
System.out.println("释放资源");
}
}
信号量实现互斥(二元信号量)
当信号量计数器初始值为 1 时,即为“二元信号量”,可实现与锁相同的互斥效果,保证同一时间只有一个线程访问资源。
import java.util.concurrent.Semaphore;
public class Demo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 初始化二元信号量,计数器初始值为 1
Semaphore semaphore = new Semaphore(1);
Thread t1 = new Thread(()-> {
try {
for (int i = 0; i < 50000; i++) {
semaphore.acquire(); // 申请唯一资源,实现互斥
count++;
semaphore.release(); // 释放资源
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t2 = new Thread(()-> {
try {
for (int i = 0; i < 50000; i++) {
semaphore.acquire(); // 申请唯一资源,实现互斥
count++;
semaphore.release(); // 释放资源
}
} catch (Exception e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
CountDownLatch 等待组
CountDownLatch 用于实现“等待所有线程完成任务后,再执行后续逻辑”,核心是一个递减计数器,线程完成任务后计数器减 1,主线程等待计数器归 0 后继续执行。
import java.util.concurrent.CountDownLatch;
public class Demo {
public static void main(String[] args) throws InterruptedException {
// 初始化计数器,值为 8(表示需要等待 8 个线程完成任务)
CountDownLatch latch = new CountDownLatch(8);
for (int i = 0; i < 8; i++) {
int id = i;
Thread t = new Thread(()-> {
try {
System.out.println("线程开始" + id);
Thread.sleep(1000); // 模拟任务执行
System.out.println("线程结束" + id) ;
latch.countDown(); // 线程完成任务,计数器减 1
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
// 阻塞等待,直到计数器归 0
latch.await();
System.out.println("所有任务都结束");
}
}
线程安全的集合类
Vector、Stack、Hashtable 是早期 Java 提供的线程安全集合,但性能较差,不推荐在高并发场景使用;其他常用集合(ArrayList、HashMap、LinkedList 等)均为线程不安全,高并发场景需选择合适的线程安全替代方案。
多线程使用 ArrayList
- 手动加锁:使用
synchronized或ReentrantLock修饰对 ArrayList 的操作方法,保证互斥访问。 - 工具类包装:使用
Collections.synchronizedList(new ArrayList<>()),返回一个线程安全的 ArrayList 包装类(底层通过synchronized实现,性能一般)。 - CopyOnWriteArrayList:通过“写时拷贝”实现线程安全,读操作直接访问原数组无需加锁,写操作时复制原数组副本,在副本上完成修改后切换数组引用,且写操作加锁保证互斥。适合读多写少、数组规模较小的场景(如配置缓存)。
多线程使用队列
推荐使用并发包提供的阻塞队列,自带线程安全和阻塞特性,适合生产者-消费者模型:
- ArrayBlockingQueue:基于数组实现的有界阻塞队列,性能稳定。
- LinkedBlockingQueue:基于链表实现的阻塞队列,可指定容量(默认无界,需避免 OOM)。
- PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列,支持按优先级排序。
- SynchronousQueue:不存储元素的阻塞队列,生产者提交任务必须等待消费者接收,适合线程间直接传递数据。
多线程使用哈希表
- Hashtable:早期线程安全哈希表,通过
synchronized修饰所有方法,整个对象一把锁,锁冲突概率高,性能差,不推荐使用。 - ConcurrentHashMap:并发包提供的高效线程安全哈希表,是 HashMap 的线程安全替代方案:
- 分段锁(JDK 1.7)/ 桶锁(JDK 1.8):细化锁粒度,减少锁冲突,提升并发性能。
- 大小更新:通过 CAS 操作更新哈希表大小,避免加锁开销。
- 扩容优化:采用“分批扩容”,在
put()/get()操作中逐步迁移数据,避免一次性扩容带来的性能抖动。