多线程(2)

多线程(2)

一、CAS

CAS 是乐观锁最核心、最底层的实现机制,全称是 Compare And Swap(比较并交换),它是一条 CPU 级别的原子指令,能保证操作的原子性,也是很多无锁编程的基础。

核心逻辑

CAS 操作包含三个核心参数:

  1. 内存地址(V):要操作的共享变量在内存中的位置;
  2. 预期值(A):线程读取到的共享变量的原始值;
  3. 新值(B):线程想要把共享变量更新成的目标值。

它的执行逻辑可以通俗理解为:

线程在更新共享变量前,先去内存地址 V 处查当前值。如果这个值和我之前读到的预期值 A 完全一样,说明没人改过,我就把它换成新值 B;如果不一样,说明已经被其他线程改了,这次更新就失败,我不做任何操作。

整个过程是原子性的——CPU 会保证这个“比较+交换”的动作要么全做完,要么全不做,不会被其他线程打断,这也是它能替代锁的关键。

执行流程

举个生活中的例子:你想给手机充话费(共享变量是话费余额):

  1. 你先查了下余额,显示还有 10 元(这就是「预期值 A」);
  2. 你准备充 50 元,想把余额改成 60 元(这就是「新值 B」);
  3. 你提交充值操作时,系统会先去查当前余额(「内存地址 V」的实际值):
    • 如果实际余额还是 10 元,说明没人中途操作,就把余额改成 60 元,充值成功;
    • 如果实际余额已经不是 10 元(比如家人先给你充了 100 元,余额变成 110),说明数据被改了,充值操作失败,你可以选择重新查余额再试,或者放弃。

特点与应用

核心特点

  • 无锁:不需要像悲观锁那样阻塞线程,线程更新失败时只是重试或放弃,开销远小于锁;
  • 原子性:由 CPU 指令保证,无需额外的同步手段;
  • 非阻塞:即使更新失败,当前线程也不会被挂起,能继续执行其他逻辑。

典型应用

  • 编程语言层面:Java 中的 java.util.concurrent.atomic 包(比如 AtomicIntegerAtomicLong)就是基于 CAS 实现的,比如 AtomicInteger.incrementAndGet()(自增)就是通过 CAS 完成,替代了加锁的 i++
  • 数据库层面:乐观锁的版本号更新(update ... where version = ?)本质上是 CAS 思想的体现;
  • 分布式系统:Redis 的 SETNX 命令、分布式锁的实现也用到了 CAS 思想。

局限性

  1. ABA 问题:这是最典型的问题——如果变量值从 A 被改成 B,又改回 A,CAS 会认为值没变化,但实际上已经被修改过。解决办法是给变量加版本号/时间戳(比如 Java 的 AtomicStampedReference),不仅比较值,还比较版本;
  2. 自旋开销:如果高并发下更新频繁失败,线程会不断重试(自旋),会消耗 CPU 资源;
  3. 只能操作单个变量:CAS 只能保证单个变量操作的原子性,如果要操作多个变量,还是需要借助锁或其他方式(比如把多个变量封装成一个对象)。

二、多种锁

悲观锁

核心思想

悲观地认为并发操作一定会发生冲突,因此在整个数据处理过程中,会强制锁定资源,阻止其他线程对资源的访问,直到当前线程操作完成。

可以理解为:“凡事往最坏的方向想,先锁住资源,再干活,避免别人捣乱”。

实现方式

  • 数据库层面:使用 SELECT ... FOR UPDATE 语句,查询数据时直接锁定对应的行,其他事务无法对其进行修改或加锁,直到当前事务提交或回滚。
  • Java层面:使用 synchronized 关键字,或者 ReentrantLock 等独占锁,同一时刻只有一个线程能执行被锁定的代码块。

适用场景

  • 并发冲突概率高的场景,比如库存扣减、转账等核心业务逻辑。
  • 对数据一致性要求极高的场景,不允许出现脏读、不可重复读等问题。

优缺点

优点缺点
实现简单,数据一致性强性能开销大,容易出现死锁
冲突处理直接,无需重试会阻塞其他线程,并发效率低

乐观锁

核心思想

乐观地认为并发操作大概率不会发生冲突,因此不会主动锁定资源,而是在更新数据时,检查数据是否被其他线程修改过。如果未被修改,则正常更新;如果已被修改,则放弃当前操作或重试。

可以理解为:“凡事往最好的方向想,先干活,最后再检查有没有人动过数据”。

实现方式

  1. 版本号机制

    • 数据库表中增加一个 version 字段,初始值为 0
    • 读取数据时,同时读取 version 的值。
    • 更新数据时,执行 UPDATE ... WHERE id = ? AND version = ?,并将 version1
    • 检查更新影响的行数,如果为 0,说明数据已被其他线程修改,执行重试或返回失败。
  2. CAS 机制

    • 是一种无锁算法,包含三个核心参数:内存地址V预期值A新值B
    • 原理:只有当内存地址 V 中的值等于预期值 A 时,才会将其更新为 B,这个操作是原子性的;如果不相等,则说明值已被修改,放弃更新或重试。
    • Java中的 AtomicIntegerAtomicReference 等原子类就是基于CAS实现的。

适用场景

  • 并发冲突概率低的场景,比如用户资料修改、文章点赞等非核心业务。
  • 追求高并发性能,可以接受少量冲突重试的场景。

优缺点

优点缺点
不阻塞线程,并发效率高冲突时需要重试,增加业务复杂度
没有锁的开销,性能好存在ABA问题(数据被修改后又改回原值,CAS会误判未修改)
高冲突场景下,重试次数过多会导致性能下降

说明:重量级锁和轻量级锁是 Java 中 synchronized 关键字在不同并发场景下的两种锁实现形态,核心区别在于是否依赖操作系统的互斥量,以及对性能开销的影响。二者的设计目的是为了在不同并发压力下,让 synchronized 达到最优性能——这也是 JDK 1.6 对 synchronized 做的核心优化(之前的 synchronized 只有重量级锁)。


轻量级锁

核心思想

基于“大多数情况下,锁不会存在多线程竞争”的假设,采用无锁的 CAS 操作来实现加锁/解锁,避免调用操作系统的内核态函数,从而降低性能开销。

实现原理

  1. 加锁阶段
    • 当线程进入同步代码块时,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record)对象,用于存储锁对象的 Mark Word(对象头中的标记字段)的拷贝。
    • 线程通过 CAS 操作,将锁对象的 Mark Word 更新为指向自身锁记录的指针。
    • 如果 CAS 成功,线程就持有了该轻量级锁;如果失败(说明有其他线程竞争),则膨胀为重量级锁。
  2. 解锁阶段
    • 线程退出同步代码块时,通过 CAS 操作将锁对象的 Mark Word 恢复为原始值。
    • 如果 CAS 成功,解锁完成;如果失败,说明锁已经膨胀,需要唤醒被阻塞的线程。

适用场景

  • 多线程交替执行同步代码块的场景(几乎没有锁竞争)。
  • 锁持有时间很短的场景。

优缺点

优点缺点
基于用户态 CAS 操作,开销小一旦出现锁竞争,会膨胀为重量级锁,产生额外开销
不会阻塞线程竞争激烈时,性能不如重量级锁

重量级锁

核心思想

基于“存在多线程竞争”的假设,依赖操作系统的互斥量实现加锁,会导致线程在用户态和内核态之间切换,性能开销较大。

实现原理

  1. 加锁阶段
    • 当轻量级锁的 CAS 操作失败(出现竞争),JVM 会将锁升级为重量级锁。
    • 锁对象的 Mark Word 会被更新为指向一个互斥量的指针。
    • 竞争锁失败的线程会被阻塞,并放弃 CPU 执行权,进入阻塞队列。
  2. 解锁阶段
    • 持有锁的线程释放锁时,会唤醒阻塞队列中的线程,这些线程会重新竞争锁。

适用场景

  • 多线程同时竞争锁的场景(高并发冲突)。
  • 锁持有时间较长的场景(比如同步代码块内有 IO 操作、sleep 等)。

优缺点

优点缺点
适合高竞争场景,稳定性强线程阻塞/唤醒需要切换内核态,开销大
不会产生大量 CAS 重试消耗存在线程上下文切换的性能损耗

自旋锁

核心思想

自旋锁是一种非阻塞的轻量级锁机制,核心思想是:当线程竞争锁失败时,不立即进入阻塞状态,而是通过循环(自旋)不断尝试获取锁,直到成功获取锁,或者达到预设的自旋次数上限后再放弃。

核心设计逻辑

  • 基于假设:锁被持有的时间很短,线程通过短暂自旋就能拿到锁,这样可以避免线程从用户态切换到内核态的巨大开销(线程阻塞/唤醒需要内核态操作)。
  • 本质:用 CPU 时间换取线程切换的开销,适合锁持有时间短、并发冲突不激烈的场景。

实现原理

在 Java 中,自旋锁是 synchronized 锁升级流程中的一个环节,具体过程如下:

  1. 当轻量级锁的 CAS 操作失败(出现线程竞争),JVM 不会直接将锁升级为重量级锁,而是先触发自旋。
  2. 竞争锁失败的线程会进入一个循环,不断执行 CAS 操作尝试获取锁。
  3. 自旋过程中,线程处于 Runnable 状态,不会释放 CPU 资源。
  4. 存在两种结果:
    • 自旋成功:在自旋次数上限内拿到锁,继续执行同步代码块。
    • 自旋失败:达到自旋次数上限仍未拿到锁,此时锁会膨胀为重量级锁,该线程会被挂起,进入阻塞队列。

关键特性

特性说明
非阻塞竞争失败时不阻塞,而是自旋尝试
CPU 消耗自旋时会占用 CPU 资源,若锁持有时间长,会造成 CPU 空转浪费
自旋次数JDK 中默认自旋次数是10 次,也可以通过参数 -XX:PreBlockSpin 手动设置
自适应自旋JDK 1.6 引入自适应自旋,自旋次数不再固定,而是根据前一次获取该锁的自旋时间和锁的拥有者状态动态调整(比如之前自旋 5 次成功,这次也大概率自旋 5 次)

适用场景 vs 不适用场景

适用场景不适用场景
锁持有时间极短(如几纳秒/微秒)锁持有时间长(如包含 IO 操作、sleep)
并发线程数不多并发线程数远大于 CPU 核心数(会导致大量线程自旋,CPU 被耗尽)
多核 CPU 环境(有多余核心支持自旋)单核 CPU 环境(自旋会占用唯一的 CPU 资源,导致其他线程无法执行)

与其他锁的关系

  1. 和轻量级锁的关系:自旋锁是轻量级锁竞争时的补充策略,用于延缓锁升级为重量级锁的时机。
  2. 和重量级锁的关系:自旋失败后,锁会升级为重量级锁,线程从自旋转为阻塞。
  3. 和乐观锁的关系:二者都基于 CAS 操作,但乐观锁是无锁的思想(不锁定资源,只在更新时检查),自旋锁是有锁的思想(必须拿到锁才能执行)。

公平锁

核心思想

严格遵循FIFO(先进先出)原则:所有竞争锁的线程会按请求锁的先后顺序排队,只有队列头部的线程能获取锁,新请求锁的线程必须排到队列末尾等待,不允许「插队」。

可以理解为:“排队买票,先来的先买,后到的必须排最后,绝对不允许加塞”。

实现原理

  1. 当线程请求锁时,首先检查锁是否空闲:
    • 若锁空闲,且等待队列为空,则直接获取锁;
    • 若锁空闲,但等待队列有线程,则当前线程加入队列尾部等待。
  2. 当持有锁的线程释放锁时,会唤醒等待队列头部的线程,让其获取锁。

优缺点

优点缺点
绝对公平,每个线程都能按顺序获取锁,不会出现「线程饥饿」(某个线程一直拿不到锁)性能开销大:线程切换和队列维护会增加额外开销
避免长时间等待的线程一直抢不到锁吞吐量低:即使锁刚释放且当前线程正好请求,也必须排队,无法利用「空窗期」

适用场景

  • 对公平性要求极高的场景(如金融交易、任务调度);
  • 线程持有锁时间较长,或线程请求锁的频率较低的场景(此时公平性带来的性能损耗可接受)。

非公平锁

核心思想

不遵循严格的排队顺序:线程请求锁时,会先尝试直接抢占锁(不排队),只有抢占失败时,才会加入等待队列尾部。即使等待队列中有线程在排队,新请求的线程也可能「插队」成功。

可以理解为:“买票时,先冲上去试试窗口有没有空位,有空位就直接买,没空位再排队;甚至有人刚买完票,你正好冲上去,就不用排队了”。

实现原理

  1. 当线程请求锁时,首先尝试通过 CAS 操作直接获取锁(无视等待队列);
  2. 若抢占成功,直接持有锁;若抢占失败,再加入等待队列尾部等待;
  3. 当持有锁的线程释放锁时,唤醒队列头部线程的同时,新请求锁的线程仍可能抢占,导致被唤醒的线程再次竞争失败。

优缺点

优点缺点
性能高、吞吐量大:减少线程切换和队列调度开销,能利用锁释放的「空窗期」不公平:可能出现「线程饥饿」,某些线程长时间排队却一直被新线程插队抢锁
是大多数场景的默认选择(如 ReentrantLock 无参构造、synchronized极端情况下,队列尾部线程可能长期无法获取锁

适用场景

  • 对性能要求高于公平性的场景(如普通业务逻辑、高并发接口);
  • 线程持有锁时间极短,或线程请求锁频率高的场景(此时抢占的收益远大于公平性损耗)。

读写锁

核心思想

读写锁是一种适用于读多写少场景的共享-独占锁,核心设计目标是在保证数据一致性的前提下,提升并发读取的效率。它将对资源的访问分为读操作和写操作两种类型,分别对应不同的锁规则。

读写锁遵循“多读单写”原则:

  • 读锁(共享锁):多个线程可以同时持有读锁,并发读取资源,互不阻塞。
  • 写锁(独占锁):只有一个线程能持有写锁,写锁会排斥所有读锁和其他写锁,即写操作与读操作、写操作与写操作之间都是互斥的。

简单理解:读和读可以共存,读和写、写和写互斥。

实现原理

Java 中的 ReentrantReadWriteLock 是读写锁的典型实现,它内部维护了两把锁:

  1. 读锁(ReadLock
    • 当线程请求读锁时,若当前没有线程持有写锁,且没有线程在等待写锁(非公平模式),则直接获取读锁;
    • 读锁的持有数量会被计数,一个线程可重复获取读锁(可重入),释放次数需与获取次数一致。
  2. 写锁(WriteLock
    • 当线程请求写锁时,必须满足没有任何线程持有读锁或写锁,才能成功获取;
    • 写锁也是可重入的,持有写锁的线程可以再获取读锁(降级),但持有读锁的线程不能直接获取写锁(升级)。

核心特性

  1. 锁的互斥规则

    操作场景是否互斥
    读锁 vs 读锁不互斥(并发读取)
    读锁 vs 写锁互斥(读时不能写,写时不能读)
    写锁 vs 写锁互斥(同一时间只能一个写操作)
  2. 可重入性

    • 读锁可重入:一个线程获取读锁后,可再次获取读锁,释放时需对应次数。
    • 写锁可重入:一个线程获取写锁后,可再次获取写锁,也可获取读锁(锁降级)。
  3. 锁降级
    指写锁降级为读锁的过程,步骤为:

    1. 线程持有写锁;
    2. 线程获取读锁;
    3. 线程释放写锁;
      最终线程持有读锁。

    注意:不支持锁升级(读锁不能直接升级为写锁),否则会导致死锁。

三、synchronized 原理

synchronized 是乐观锁也是悲观锁,是轻量级锁也是重量级锁,是挂起等待锁也是自旋锁,是不公平锁、可重入锁,不是读写锁。

加锁的工作过程

synchronized 是 Java 中最基础的互斥同步锁,JDK 1.6 对其做了重大优化(引入了偏向锁、轻量级锁、重量级锁),核心是从无锁到偏向锁,再到轻量级锁,最后升级为重量级锁的“锁升级”过程,目的是减少重量级锁带来的性能开销。

偏向锁

  • 适用场景:单线程重复获取同一把锁(最常见的场景)。
  • 加锁过程:
    1. 线程第一次获取锁时,JVM 会修改对象头的 Mark Word:将偏向锁标识设为 1,记录当前线程的 ID,无需 CAS 操作,开销极低。
    2. 后续该线程再次获取锁时,只需检查 Mark Word 中的线程 ID 是否是自己,无需任何同步操作,直接获取锁。
  • 解锁过程:偏向锁不会主动释放,只有当其他线程尝试竞争锁时,才会触发偏向锁的撤销(需要暂停持有锁的线程,将偏向锁升级为轻量级锁)。

轻量级锁

  • 适用场景:多个线程交替获取锁(无激烈竞争)。
  • 加锁过程:
    1. 线程在自己的栈帧中创建一个 Lock Record(锁记录),存储对象头 Mark Word 的拷贝。
    2. 通过 CAS 操作将对象头的 Mark Word 替换为指向当前线程 Lock Record 的指针。
    3. CAS 成功则获取锁;失败则检查对象头是否指向自己的 Lock Record(重入),是则直接获取,否则升级为重量级锁。
  • 解锁过程:
    1. 通过 CAS 将对象头的 Mark Word 恢复为原始值。
    2. CAS 成功则解锁完成;失败则说明有其他线程竞争,需唤醒等待线程。

重量级锁

  • 适用场景:多个线程同时竞争锁(激烈竞争)。
  • 加锁过程:
    1. JVM 为对象创建一个 Monitor(监视器,底层依赖操作系统的互斥量 Mutex)。
    2. 线程获取锁时,会进入 Monitor 的等待队列,若获取失败则被操作系统挂起(从用户态切换到内核态,开销极大)。
  • 解锁过程:
    1. 线程释放锁时,唤醒 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 接口

CallableRunnable 类似,都是用于定义线程任务的接口,但 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 的区别

  1. 使用方式:synchronized 搭配代码块使用,自动加解锁;ReentrantLock 调用 lock()/unlock() 手动加解锁,需配合 finally 保证解锁。
  2. 实现层面:synchronized 是 Java 关键字,底层由 JVM 通过 C++ 实现;ReentrantLock 是并发包的类,核心逻辑由 Java 实现(底层依赖系统 API)。
  3. 额外功能:ReentrantLock 提供 tryLock() 方法(支持超时获取锁、立即返回获取结果),还支持公平锁实现。
  4. 条件变量: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

  1. 手动加锁:使用 synchronizedReentrantLock 修饰对 ArrayList 的操作方法,保证互斥访问。
  2. 工具类包装:使用 Collections.synchronizedList(new ArrayList<>()),返回一个线程安全的 ArrayList 包装类(底层通过 synchronized 实现,性能一般)。
  3. CopyOnWriteArrayList:通过“写时拷贝”实现线程安全,读操作直接访问原数组无需加锁,写操作时复制原数组副本,在副本上完成修改后切换数组引用,且写操作加锁保证互斥。适合读多写少、数组规模较小的场景(如配置缓存)。

多线程使用队列

推荐使用并发包提供的阻塞队列,自带线程安全和阻塞特性,适合生产者-消费者模型:

  1. ArrayBlockingQueue:基于数组实现的有界阻塞队列,性能稳定。
  2. LinkedBlockingQueue:基于链表实现的阻塞队列,可指定容量(默认无界,需避免 OOM)。
  3. PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列,支持按优先级排序。
  4. SynchronousQueue:不存储元素的阻塞队列,生产者提交任务必须等待消费者接收,适合线程间直接传递数据。

多线程使用哈希表

  1. Hashtable:早期线程安全哈希表,通过 synchronized 修饰所有方法,整个对象一把锁,锁冲突概率高,性能差,不推荐使用。
  2. ConcurrentHashMap:并发包提供的高效线程安全哈希表,是 HashMap 的线程安全替代方案:
    • 分段锁(JDK 1.7)/ 桶锁(JDK 1.8):细化锁粒度,减少锁冲突,提升并发性能。
    • 大小更新:通过 CAS 操作更新哈希表大小,避免加锁开销。
    • 扩容优化:采用“分批扩容”,在 put()/get() 操作中逐步迁移数据,避免一次性扩容带来的性能抖动。
多线程(1) 2026-01-17

评论区