Java 多线程编程

一、线程基础概念

1.1 进程与线程的区别

jin cheng
进程:操作系统资源分配的基本单位,有独立的内存空间
线程:CPU调度的基本单位,共享进程的内存空间
多线程:一个进程内同时运行多个线程,并发 / 并行执行任务。
一个进程包含多个线程,Java程序至少有一个主线程(main线程)
核心区别:
并发:单核 CPU,线程交替快速切换执行(看起来同时跑)。
并行:多核 CPU,线程真正同时执行。

1.2 Java线程的优势

1. 充分利用多核CPU
2. 提高程序响应性
3. 更高效的资源利用

1.3 多线程在工作中的使用

1)接口响应优化:批量发送短信 / 邮件、批量导出报表。
2)异步处理:日志记录、消息推送、数据同步。
3)高并发:秒杀、订单处理、服务器同时处理上千请求。
4)资源利用:充分利用多核 CPU,避免单核空闲。

二、线程的创建与启动

2.1 继承Thread类

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的业务逻辑
        System.out.println("线程:" + Thread.currentThread().getName() + " 运行中");
    }
}

// 测试
public class Test {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.setName("线程-1");
        t1.start(); // 启动线程(必须调用 start(),不能直接调用 run())
    }
}

实际工作注意事项:

  • 线程类命名规范:XXXThread或 XXXWorker

  • 避免在构造函数中启动线程

  • 优先实现Runnable,避免单继承限制

  • 直接调用 run() 是普通方法调用,不会开启新线程!

  • 继承 Thread 有局限:Java 单继承,无法再继承其他类。

2.2 实现Runnable接口(推荐)

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程:" + Thread.currentThread().getName() + " 运行中");
    }
}

// 测试
public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(), "线程-2");
        t1.start();
    }
}

优点:避免单继承局限,适合多个线程共享同一个任务。

2.3 实现 Callable 接口(有返回值 + 抛异常)

前两种都无返回值、不能抛检查异常,Callable 可以解决。

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 模拟计算
        Thread.sleep(1000);
        return 100 + 200;
    }
}

// 测试
public class Test {
    public static void main(String[] args) throws Exception {
        FutureTask<Integer> task = new FutureTask<>(new MyCallable());
        Thread t1 = new Thread(task, "线程-3");
        t1.start();
        
        // 获取返回值(阻塞等待线程执行完)
        Integer result = task.get();
        System.out.println("结果:" + result);
    }
}
// 实际工作场景:需要返回结果的异步任务
public class MyCallable implements Callable<String> {
    private int taskId;
    
    public MyCallable(int taskId) {
        this.taskId = taskId;
    }
    
    @Override
    public String call() throws Exception {
        // 实际业务:如查询数据库、调用外部接口
        Thread.sleep(1000);
        return "任务#" + taskId + "完成";
    }
}

// 配合ExecutorService使用
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> future = executor.submit(new MyCallable(1));
String result = future.get();  // 阻塞获取结果

工作场景:需要异步计算结果、批量任务汇总结果

三、线程状态与生命周期

3.1 线程的6种状态

Java 线程只有 6 种状态,在 Thread.State 枚举中定义:
1)NEW(新建):创建了 Thread 对象,还没调用 start()。
2)RUNNABLE(就绪 / 运行):调用了 start(),等待 CPU 调度 或 正在执行。
3)BLOCKED(阻塞):等待锁(synchronized),抢不到锁就阻塞。
4)WAITING(无限等待):调用 wait()/join()/LockSupport.park(),必须被其他线程唤醒。
5)TIMED_WAITING(限时等待):调用 sleep(ms)/wait(ms),时间到自动唤醒。
6)TERMINATED(终止):线程执行完毕 / 异常退出。

状态流转细节:

1)NEW → RUNNABLE:调用 start()
2)RUNNABLE → BLOCKED:等待 synchronized 锁
3)RUNNABLE → WAITING:无参 wait/join
4)RUNNABLE → TIMED_WAITING:带时间的 sleep/wait/join
5)任何状态(除 NEW)→ TERMINATED:执行结束 / 异常

线程转换详情实例代码:

public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                // 进入TIMED_WAITING
                Thread.sleep(1000);
                
                synchronized (ThreadStateDemo.class) {
                    // 持有锁
                    ThreadStateDemo.class.wait();  // 进入WAITING
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        System.out.println("创建后状态:" + thread.getState());  // NEW
        
        thread.start();
        Thread.sleep(100);
        System.out.println("启动后状态:" + thread.getState());  // TIMED_WAITING
        
        Thread.sleep(2000);
        System.out.println("sleep后状态:" + thread.getState());  // WAITING
        
        thread.interrupt();
        Thread.sleep(100);
        System.out.println("终止前状态:" + thread.getState());  // TERMINATED
    }
}

四、线程的核心方法

4.1 sleep () vs wait ()(高频面试 + 工作必用)

方法 所属类 锁资源 使用场景
sleep(long) Thread 不释放锁 暂停当前线程,延时执行
wait() Object 释放锁 线程间通信、等待唤醒

关键细节:

sleep 是线程休眠,时间到自动恢复,不依赖唤醒。

wait 必须在同步代码块(synchronized) 中使用,必须用 notify/notifyAll 唤醒。

4.2 start() vs run()

start():开启新线程,JVM 自动调用 run()。
run():只是普通方法,不会开启线程。

4.3 join ():等待线程执行完成

作用:让调用线程等待该线程执行完毕再继续。

工作场景:主线程需要等待子线程计算完成再汇总。

Thread t1 = new Thread(() -> {
    try { Thread.sleep(1000); } catch (Exception e) {}
});
t1.start();
t1.join(); // 主线程阻塞,等待 t1 执行完
System.out.println("t1 执行完毕");

4.4 yield ():让出 CPU 执行权

作用:当前线程让出 CPU,回到就绪状态,不释放锁。

工作中极少用,不稳定。

4.5 interrupt ():中断线程

不是强制停止线程,只是设置中断标志位。

正确用法:

Thread t1 = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 执行业务
    }
});
t1.start();
t1.interrupt(); // 设置中断标志,优雅停止

五、线程安全与同步

5.1 竞态条件Race Condition)

// 实际工作场景:库存扣减、计数器等
public class UnsafeCounter {
    private int count = 0;
    
    public void increment() {
        count++;  // 非原子操作:读取-修改-写入
    }
    
    public int getCount() {
        return count;
    }
}
// 并发执行时会导致数据不一致

5.2 synchronized关键字

synchronized(内置锁)

作用:保证原子性、可见性、有序性,同一时间只有一个线程执行。

用法:

1、修饰实例方法:锁当前对象

2、修饰静态方法:锁当前类

3、修饰代码块:锁指定对象(推荐,粒度更小)

// 1. 同步方法
public class SafeCounter {
    private int count = 0;
    
    // 实例方法锁:锁当前对象
    public synchronized void increment() {
        count++;
    }
    
    // 静态方法锁:锁类对象
    public static synchronized void staticMethod() {
        // ...
    }
}

// 2. 同步代码块(更细粒度控制)
public class OptimizedCounter {
    private int count = 0;
    private final Object lock = new Object();  // 专门用于锁的对象
    
    public void increment() {
        // 只同步必要的代码
        synchronized (lock) {
            count++;
        }
        // 其他非线程安全操作
    }
}

实际工作注意事项:

  • 避免在synchronized块中调用耗时操作

  • 锁的粒度要尽可能小

  • 避免锁嵌套,防止死锁

  • 使用专门的锁对象,不要锁字符串常量或基础类型

5.3 Lock 接口(JDK 1.5+ 显式锁)

比 synchronized 更灵活:可尝试锁、可超时、可公平锁。

常用实现:ReentrantLock

Lock lock = new ReentrantLock();

public void sale() {
    lock.lock(); // 加锁
    try {
        if (count > 0) {
            count--;
        }
    } finally {
        lock.unlock(); // 必须在 finally 释放锁!
    }
}

工作原则:
简单场景用 synchronized(简洁、JVM 优化好)。

需要灵活控制(尝试获取锁、超时)用 Lock。

5.4 解决方案 3:ThreadLocal(线程本地变量)

作用:每个线程独立拥有一份变量副本,互不干扰,无锁安全。

工作场景:用户登录信息、数据库连接、事务上下文。

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

// 每个线程操作自己的值,绝对安全

5.5 volatile关键字

1. volatile 是什么?
volatile 是 Java 轻量级同步关键字,修饰成员变量、静态变量;
不能修饰局部变量、不能修饰方法、不能修饰类。

核心三大能力(精准版):
1)保证多线程可见性
2)禁止指令重排序
3)不保证原子性
4)不具备互斥性(不能锁代码块)

2. 学习 volatile 的背景 --> Java 内存模型 JMM?
JMM 内存结构
1)主内存:共享变量存在这里
2)工作内存:每个线程独有缓存(CPU 缓存、寄存器)
规则:
1)线程不能直接读写主内存
2)线程先把主内存变量拷贝到自己工作内存
3)修改只改自己工作内存,不一定立刻刷回主内存
4)别的线程也只读自己缓存,看不到别人修改 → 可见性问题

3. 普通变量存在的问题
boolean flag = true;

// 线程1
while (flag) {
    // 死循环
}

// 线程2 稍后执行
flag = false;
现象:
线程 2 改了 flag=false,线程 1 永远感知不到,一直死循环。
原因:
线程 1 把 flag 缓存在工作内存,不再去主内存刷新读取。

4. volatile 第一大作用:保证可见性
1)volatile 修饰变量后的规则
读操作:每次都强制从主内存读取最新值,不使用线程本地缓存
写操作:修改后强制立刻刷新回主内存,清空本地缓存
其他线程下次读,必须从主内存拿新值
2)改造成 volatile 就正常
volatile boolean flag = true;
线程 2 修改 → 立刻刷回主内存;
线程 1 下次循环 从主内存读新值,感知到 false,退出循环。
3)可见性底层原理:内存屏障
volatile 底层插入 CPU 内存屏障(Memory Barrier):
  写屏障:写完强制刷新到主存
  读屏障:读之前强制从主存加载
禁止指令跨屏障乱序、强制内存刷新

5. volatile 第二大作用:禁止指令重排序
1)什么是指令重排
CPU / JVM 为了执行效率,会打乱代码书写顺序,不改变单线程结果,但多线程会出 Bug。
比如:
正常的代码执行顺序:分配内存-->初始化对象-->引用赋值
CPU 可能重排成:分配内存 -->引用赋值 -->初始化对象
2)经典坑:DCL 单例不加 volatile 会空指针
// 不加 volatile 有问题
private static Singleton instance;

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                // 这里会被指令重排
                instance = new Singleton();
            }
        }
    }
    return instance;
}
new Singleton() 底层三步:
 1)开辟堆内存空间
 2)构造方法初始化对象
 3)把内存地址赋值给 instance
重排后顺序:1 → 3 → 2
其他线程可能拿到 还没初始化完的半初始化对象,直接使用报错。

3)加 volatile 解决
private static volatile Singleton instance;
volatile 禁止前后指令重排序,保证 1→2→3 顺序执行,DCL 线程安全。

6. volatile 最大短板:不保证原子性

1)什么是原子性
一次操作不可分割,要么全成功、要么全失败,中途不能被其他线程插队。
2)典型错误:volatile + i++
volatile int num = 0;  // 多线程同时执行 10000 次 num++
num++ 拆解三步:
1)从主存读 num
2)工作内存 +1
3)写回主存
volatile 只能保证每次读写可见,不能把三步合成原子操作。
多线程并发会覆盖结果,最终值小于预期。
3)怎么保证原子性
三种方案:
1)synchronized 加锁
2)Lock 可重入锁
3)原子类:AtomicInteger / AtomicLong

7. volatile 不具备的能力
1)不能保证原子性(i++ 线程不安全)
2)没有互斥锁能力
 多个线程可以同时读写 volatile 变量,不能保证同一时刻只有一个线程进入临界区
3)不能修饰方法、类、局部变量
4)不能替代 synchronized

8. volatile 典型使用场景(只这两类)
场景 1:状态标记量(停止线程、开关控制)
// 线程停止标记
volatile boolean isStop = false;

public void run() {
    while (!isStop) {
        // 业务循环
    }
}
场景 2:DCL 双重检查锁单例模式
必须用 volatile 防指令重排。
场景 3:简单读写、多线程只赋值不复合运算
只做 set / get,不做 i++、i = i+1 这种复合操作。

9. 一句话终极总结
volatile 修饰共享变量,保证可见性、禁止指令重排;
不保证原子性、没有互斥锁;
适合状态开关、DCL 单例;
复合自增、并发计算必须用 synchronized / 原子类。

10. 面试必背标准答案
volatile 是 Java 轻量级同步关键字,作用:
1)基于 JMM 内存模型,保证多线程变量可见性,强制读写主内存,不使用线程本地缓存;
2)禁止指令重排序,解决 DCL 单例重排漏洞;
3)不保证原子性,无法解决 i++ 这类复合操作线程安全问题;
4)无互斥性,仅适合状态标记、简单共享变量场景,不能替代锁。

实例演示:

// 实际场景:标志位控制、双重检查锁定
public class VolatileExample {
    // volatile保证可见性和禁止指令重排
    private volatile boolean running = true;
    
    public void start() {
        new Thread(() -> {
            while (running) {
                // 执行任务
            }
        }).start();
    }
    
    public void stop() {
        running = false;  // 其他线程立即可见
    }
}

5.6 原子类

1. Java 最经典、最高效、无锁线程安全写法 ——原子类(Atomic)
AtomicInteger、AtomicReference
无锁、线程安全、高性能,靠 CAS 机制实现,比 synchronized 快很多!

2. Atomic 最厉害的特点
1)无锁(没有 synchronized)
2)非阻塞
3)CPU 指令级原子操作
4)高并发下性能远超锁
5)完全解决 volatile 不能保证原子性的问题!

3. 最常用 Atomic 工具
AtomicInteger     // 原子int
AtomicLong        // 原子long
AtomicBoolean     // 原子布尔
AtomicReference   // 原子对象引用
AtomicStampedReference // 解决ABA问题

4. 核心原理
流程:
1)拿当前值
2)计算新值
3)比较:内存里的值 == 我刚才拿的值吗?
  相等 → 替换
  不相等 → 重来
整个过程是 CPU 原语指令,不可分割!

5. 总结
1)Atomic 系列 = 无锁、线程安全
2)靠 CAS 实现,比锁快得多
3)AtomicInteger:解决 i++ 安全问题
4)AtomicReference:安全修改对象引用
5)高并发首选,比 synchronized 轻量 N 倍!
// 实际场景:计数器、序列生成
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class AtomicExample {
    // CAS(Compare-And-Swap)实现
    private AtomicInteger counter = new AtomicInteger(0);
    private AtomicReference<String> latestData = new AtomicReference<>();
    
    public void safeIncrement() {
        // 非阻塞同步
        counter.incrementAndGet();
    }
    
    public void updateData(String newData) {
        // CAS更新
        latestData.set(newData);
    }
}

六、线程通信

多个线程需要协调执行,比如:生产者 - 消费者模式。

6.1 wait + notify/notifyAll(配合 synchronized)

// 生产者
synchronized (obj) {
    // 仓库满了
    obj.wait(); // 释放锁,等待
}

// 消费者
synchronized (obj) {
    // 消费数据
    obj.notifyAll(); // 唤醒所有等待线程
}

细节:

1) 必须使用synchronized同步

2)必须用 notifyAll(),少用 notify(),避免线程饥饿。

3)必须在循环中用 wait(),防止虚假唤醒。

// 生产者-消费者模式
public class MessageQueue {
    private Queue<String> queue = new LinkedList<>();
    private int maxSize = 10;
    
    public synchronized void produce(String message) throws InterruptedException {
        // 使用while防止虚假唤醒
        while (queue.size() == maxSize) {
            wait();  // 释放锁,进入等待
        }
        queue.offer(message);
        notifyAll();  // 通知消费者
    }
    
    public synchronized String consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        String message = queue.poll();
        notifyAll();  // 通知生产者
        return message;
    }
}

6.2 Lock + Condition(JDK 1.5+)

更灵活,可分组唤醒:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

// 等待
condition.await();

// 唤醒
condition.signalAll();

具体实例:

// 更灵活的等待/通知机制
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer<T> {
    private final T[] items;
    private int putPtr, takePtr, count;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    public void put(T x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await();  // 等待不满
            }
            items[putPtr] = x;
            if (++putPtr == items.length) putPtr = 0;
            ++count;
            notEmpty.signal();  // 通知不空
        } finally {
            lock.unlock();
        }
    }
    
    public T take() throws InterruptedException {
    lock.lock(); // 抢盘子架
    try {
        // 如果空了 → 等待
        while (count == 0) {
            notEmpty.await(); // 等“不空”信号
        }
        // 拿盘子
        T item = items[takePtr];
        // 指针循环
        if (++takePtr == items.length) takePtr = 0;
        // 数量-1
        --count;
        // 通知厨师:有空位了,可以继续放
        notFull.signal();
        return item;
    } finally {
        lock.unlock();
    }
}
}

6.3 消费者和生产者概念熟知

1. 什么是生产者 - 消费者模型?
生产者:生产数据的线程
消费者:处理数据的线程
缓冲区:生产者放数据、消费者取数据的队列
模型结构:生产者 → 缓冲区(队列)→ 消费者

2. 核心规则
缓冲区满了 → 生产者必须等待
缓冲区空了 → 消费者必须等待
生产者放入数据 → 唤醒消费者
消费者取出数据 → 唤醒生产者

3. 为什么要用这种模式?(工作场景)
场景 1:解耦(生产者和消费者不直接通信)
 1)生产者只管生产
 2)消费者只管消费
 3)互不影响,代码更清晰
场景 2:削峰 / 限流(高并发必备)
 1)突然大量请求 → 不直接处理,先放入队列
 2)消费者慢慢处理,不会压垮系统。
场景 3:异步处理(提高响应速度)
比如:
 1)用户注册 → 发送短信、发送邮件
 2)主线程不等待发送完成,丢到队列里,后台线程慢慢发
   → 接口响应极快
场景 4:快慢平衡
 1)生产者生产快,消费者处理慢 → 队列缓冲
 2)消费者处理快,生产者慢 → 消费者等待

4. 现实生活中的比喻(秒懂)
餐厅吃饭:
 1)厨师 = 生产者
 2)顾客 = 消费者
 3)餐盘架子 = 缓冲区
规则:
 1)盘子满了 → 厨师停下来等
 2)盘子空了 → 顾客等
 3)厨师放盘子 → 通知顾客
 4)顾客拿走盘子 → 通知厨师

5. 这种模式可以用在那种技术里?
 1)MQ 消息队列(RabbitMQ / RocketMQ / Kafka)
 2)线程池队列
 3)Netty 异步处理
 4)订单系统、日志系统、推送系统
 5)秒杀、高并发削峰
所有高并发、异步、解耦的系统都离不开它!

七、并发工具类

7.1 CountDownLatch

1. CountDownLatch是什么?
CountDownLatch = 倒计时门栓 / 计数器发令枪
作用:让一个线程等待,直到其他线程把计数器减到 0,它才开始运行。
核心就 3 个方法:
1)new CountDownLatch(3) → 初始化计数器 = 3
2)countDown() → 计数器 -1
3)await() → 等待计数器变成 0,再继续执行

2. 生活化比喻(秒懂)
运动会百米赛跑
  裁判 = 调用 await() 的线程 → 必须等所有运动员准备好
  运动员 = 其他线程
  3 个运动员 → new CountDownLatch(3)
  每个运动员准备好 → countDown()
  3 个都准备好 → 计数器 = 0 → 裁判鸣枪开跑!
大概流程就是:
裁判等(await)
运动员1准备好 → countDown(2)
运动员2准备好 → countDown(1)
运动员3准备好 → countDown(0)
✅ 计数器=0 → 裁判开始执行!

3. CountDownLatch的3个核心方法
1)CountDownLatch(int count)
创建计数器,必须指定等待多少个任务完成。
2)countDown()
计数器 -1
每调用一次,就代表一个线程完成了。
3)await()
阻塞等待
当前线程停在这里不动,直到计数器变成 0 才继续执行。

4. CountDownLatch 最常用两大场景
场景 1:主线程等待多个子线程全部执行完再继续
比如:
  系统启动时,等3 个配置加载线程全部完成
  等5 个接口调用全部返回
  等数据初始化完成
场景 2:模拟并发同时执行
  所有线程先 await(),主线程最后 countDown,大家一起开始执行。
  (模拟高并发压力测试)

5. CountDownLatch 特点(面试必背)
1)一次性:计数器到 0 就不能重置
2)不可重复使用:用完就废
3)一个线程等多个线程
4)底层基于 AQS 实现

6. 和 Thread.join () 的区别
join():必须等线程完全结束
CountDownLatch:更灵活,线程可以不结束,只需要调用 countDown ()

使用案例如下:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {

        // 计数器 = 3(等3个线程执行完)
        CountDownLatch latch = new CountDownLatch(3);

        // 线程1
        new Thread(() -> {
            System.out.println("线程1执行完");
            latch.countDown(); // 计数器 -1 → 2
        }).start();

        // 线程2
        new Thread(() -> {
            System.out.println("线程2执行完");
            latch.countDown(); // 计数器 -1 → 1
        }).start();

        // 线程3
        new Thread(() -> {
            System.out.println("线程3执行完");
            latch.countDown(); // 计数器 -1 → 0
        }).start();

        // 主线程等待!直到计数器=0
        System.out.println("主线程等待所有线程执行完...");
        latch.await();

        // 只有计数器=0 才会执行这里!
        System.out.println("所有线程执行完毕,主线程继续运行!");
    }
}

运行结果:

主线程等待所有线程执行完...
线程1执行完
线程2执行完
线程3执行完
所有线程执行完毕,主线程继续运行!

7.2 CyclicBarrier

1. CyclicBarrier是什么?
CyclicBarrier:循环栅栏
让一组线程互相等待,全部到达栅栏位置后,再一起同时放行;
而且可以循环复用,这是和 CountDownLatch 最大区别。
关键词:凑齐人、一起走、可循环

2. 生活化比喻(秒懂)
场景:组团旅游集合
一个旅游团固定 5 个人发车
先来的游客在景区门口等待
等到5 个人全部到齐,统一进景区
游玩结束出来,还可以再次集合、再次出发(可循环)

每一个线程 = 一个游客
栅栏屏障 = 景区门口集合点
凑齐设定数量 → 全员同时放行
用完可以重置再来一轮

3. 核心构造 & 核心方法
1)构造器
// 设定拦截线程数:凑齐 3 个就放行
CyclicBarrier barrier = new CyclicBarrier(3);

// 凑齐后,优先执行栅栏任务
CyclicBarrier barrier = new CyclicBarrier(3, ()->{
    System.out.println("所有人到齐了,统一发车!");
});
2)核心方法
// 线程到达栅栏,阻塞等待,凑齐数量才放行
barrier.await();
每个线程调用 await():表示我到栅栏了
栅栏没凑够人数:线程阻塞
凑够设定人数:所有线程同时唤醒往下执行

4. CyclicBarrier 三大核心特点
1)线程互相等待
CountDownLatch 是 一个线程等其他线程;
CyclicBarrier 是 一组线程互相等。
2)可循环复用
一轮凑齐放行后,自动重置,可以直接开启下一轮等待;
CountDownLatch 倒计时到 0 一次性作废,不能复用。
3)支持栅栏回调任务
凑齐所有人后,优先执行传入的 Runnable,再唤醒所有业务线程。

5. 真实工作常用场景
场景 1:多线程分段计算,最后合并结果
  1)开启多线程分别计算一部分数据
  2)每个线程计算完到栅栏等待
  3)全部计算完成,统一进入合并汇总阶段
场景 2:多阶段任务并行
  比如大数据处理:阶段 1 全部做完,再统一进入阶段 2,一轮一轮循环执行。
场景 3:模拟分布式节点同步
  所有节点执行到检查点,互相等待全部就绪,再统一往下走。
场景 4:压力测试模拟统一并发
  多个线程准备就绪,等全部到位再同时发起请求。

6. 底层简单原理
1)基于 AQS + 可重入锁 + 条件队列 实现
2)内部维护 parties (总人数)、count (当前剩余等待数)
3)每个 await():count--,count≠0 就阻塞
4)count=0 时:执行栅栏任务 → 唤醒所有等待线程 → 自动重置 count,支持下一轮循环

7. 面试标准背诵版
1)CyclicBarrier 是循环栅栏,让一组线程到达屏障后互相等待,凑齐数量才同时放行;
2)支持循环复用,一轮结束自动重置,区别于 CountDownLatch 一次性;
3)可传入栅栏任务,凑齐线程后优先执行;
4)适用于多线程分段计算、多阶段同步、并行任务统一推进场景;
5)底层基于 AQS 实现,利用锁和条件队列做等待唤醒。

CyclicBarrier vs CountDownLatch 重点对比

对比维度 CountDownLatch CyclicBarrier
作用 一个线程等 N 个线程完成 N 个线程互相等待,凑齐一起走
复用性 一次性,不能重置 可循环复用,自动重置
计数器递减 只能减不能加 内部自动重置
等待关系 主等子 子线程互相等
回调任务 支持凑齐后执行栅栏任务
典型场景 等待初始化、等待批量任务完成 分段任务并行、多人同步开会、多阶段任务

实例如下:

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // 凑齐3个线程就一起放行,凑齐先执行栅栏任务
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("✅ 3个线程全部到达,统一放行!");
        });

        // 开启3个线程
        for (int i = 1; i <= 3; i++) {
            int num = i;
            new Thread(() -> {
                try {
                    System.out.println("线程" + num + ":到达栅栏,等待同伴...");
                    // 阻塞等其他人
                    barrier.await();
                    System.out.println("线程" + num + ":继续往下执行业务");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

7.3 Semaphore

1. Semaphore 是什么?
Semaphore = 许可证管理器
作用:控制最多有多少个线程能同时执行某段代码!
例如:
就像厕所坑位:
  1)一共 3 个坑位
  2)最多 3 个人 同时上厕所
  3)其他人必须排队等待
  4)有人出来(释放许可证),下一个才能进去

2. 核心概念
1)构造方法
// 最多允许 3 个线程同时执行
Semaphore semaphore = new Semaphore(3); // 3 = 许可证数量
2)两个核心方法
semaphore.acquire();  // 获取许可证(没有就阻塞等待)
semaphore.release();  // 释放许可证(用完还回去)

3. 执行流程(厕所模型)
1)线程来了 → acquire() 申请许可证
2)有许可证 → 进去执行
3)没有 → 阻塞等待
4)执行完 → release() 释放许可证
5)下一个线程才能拿到许可证进去

4. Semaphore 两大核心用途(工作必用)
1)用途1: 控制并发线程数(限流)
比如:
  接口限流:最多 100 个请求同时访问
  数据库连接控制:最多 20 个连接
  避免高并发压垮系统
这就是 Semaphore 最常用场景!

2)用途2: 实现互斥锁(相当于 1 个许可证)
Semaphore semaphore = new Semaphore(1);
效果 = synchronized / ReentrantLock
同一时间只有 1 个线程执行

5. semaphore的重要特性
1)公平 / 非公平
// 非公平(默认,性能高)
new Semaphore(3);

// 公平锁:先到先得
new Semaphore(3, true);
2)acquire () 会阻塞
拿不到许可证就一直等
3)release () 必须执行
一般放在 finally 里,防止死锁
4)可以一次性获取多个许可证
semaphore.acquire(2);
semaphore.release(2);

6. Semaphore 通俗总结(背会)
Semaphore = 许可证
acquire() = 拿证
release() = 还证
控制最多多少线程同时运行
限流神器!

同步工具对比:

工具 作用 一句话记忆
CountDownLatch 一个线程等多个线程 倒计时,到 0 放行
CyclicBarrier 一组线程互相等待 凑齐人一起走
Semaphore 控制并发线程数量 许可证限流

实例如下:

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 最多 3 个线程同时执行
        Semaphore semaphore = new Semaphore(3);

        // 启动 10 个线程
        for (int i = 1; i <= 10; i++) {
            int num = i;
            new Thread(() -> {
                try {
                    // 获取许可证
                    semaphore.acquire();
                    System.out.println("线程" + num + " 正在执行");
                    Thread.sleep(2000); // 模拟业务
                    System.out.println("线程" + num + " 执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放许可证(必须放 finally)
                    semaphore.release();
                }
            }).start();
        }
    }
}

7.4 Exchanger

1. 什么是 Exchanger?
Exchanger = 两个线程之间的「数据交换站」
作用:
只有两个线程都到了,才能互相交换数据;
只要有一个没到,另一个就必须一直等!

2. 场景:两个人交换物品
线程 A = 甲
线程 B = 乙
Exchanger = 交换点
规则:
1)甲带着数据来到交换点 → 等待乙
2)乙带着数据来到交换点 → 等待甲
3)两个人都到了 → 瞬间交换数据
4)各自拿着对方的数据离开

3. 核心 API(就一个方法)
Exchanger<String> exchanger = new Exchanger<>();

// 线程调用这个方法:把自己的数据交出去,等待并获取对方的数据
String otherData = exchanger.exchange(myData);
exchange (我的数据)
  阻塞等待另一个线程到来
  对方到了 → 交换数据
  交换完成 → 两个线程同时唤醒

4. 代码执行流程(超级清晰)
1)线程 A 先到交换点
  调用 exchange()
  阻塞等待,直到线程 B 来
2)线程 B 后到交换点
  调用 exchange()
  两个线程相遇
3)瞬间交换数据
  A 把自己的数据给 B
  B 把自己的数据给 A
4)交换完成
  两个线程同时唤醒
  各自拿到对方的数据继续执行

4. Exchanger 三大特点(面试必背)
1)只能用于 两个线程 交换
第三个线程来会一直阻塞,永远交换不了。
2)双向阻塞
一个线程到了必须等另一个,谁先到谁等。
3)无锁、高效
底层使用 CAS 实现,比加锁交换快很多。
4)可以循环交换
交换完一次,还能继续交换多次。

5. 使用场景(工作中用在哪?)
场景 1:两个线程之间交换数据
 生产者 A → 给 → 消费者 B
 生产者 B → 给 → 消费者 A
 一对一交换
场景 2:线程间实时传输数据
 游戏:玩家交易
 数据流交换
 管道通信
场景 3:错误恢复 / 数据校对
 一个线程计算一半,把中间结果交给另一个线程继续算。

Java 四大同步工具对比总结:

工具 作用 一句话
CountDownLatch 一个线程等多个线程 倒计时到 0 放行
CyclicBarrier 一组线程互相等待 凑齐人一起走
Semaphore 控制并发数量 许可证限流
Exchanger 两个线程交换数据 你换我,我换你

实用案例演示:

import java.util.concurrent.Exchanger;

public class ExchangerDemo {
    public static void main(String[] args) {
        // 创建交换器
        Exchanger<String> exchanger = new Exchanger<>();

        // 线程A
        new Thread(() -> {
            try {
                String dataA = "我是A的数据";
                System.out.println("线程A 到达交换点,等待对方...");
                
                // 交换:把A的数据交出去,拿到B的数据
                String dataB = exchanger.exchange(dataA);
                
                System.out.println("线程A 收到:" + dataB);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();

        // 线程B
        new Thread(() -> {
            try {
                String dataB = "我是B的数据";
                System.out.println("线程B 到达交换点,等待对方...");
                
                // 交换:把B的数据交出去,拿到A的数据
                String dataA = exchanger.exchange(dataB);
                
                System.out.println("线程B 收到:" + dataA);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();
    }
}

输出结果:

线程A 到达交换点,等待对方...
线程B 到达交换点,等待对方...
线程A 收到:我是B的数据
线程B 收到:我是A的数据

用通俗话理解:
线程 A 先到交换点 → 阻塞等待
线程 B 后到交换点 → 触发交换
互相交换数据
两个线程同时被唤醒,继续运行

八、线程池

8.1 为什么要用线程池?

1. 不用线程池的坏处
手动 new Thread() 弊端:
 1)频繁创建销毁线程 开销极大
 2)无限制创建线程,CPU 爆满、OOM
 3)线程无管理,不可监控、不可复用
 4)任务来了就新建,高并发直接打垮系统

2. 线程池核心好处(工作实际价值)
 1)线程复用:线程创建一次,反复处理任务,减少开销
 2)控制并发数:限制最大线程数,保护 CPU、不雪崩
 3)统一管理:线程统一调度、监控、拒绝策略
 4)异步解耦:业务异步处理,提升接口响应速度
 5)定时 / 周期任务:支持延迟、定时执行

一句话:工作中绝不允许手动 new Thread,一律用线程池。

8.2 线程池核心底层架构

1. 核心组成
 1)核心线程:常驻线程,不随便回收
 2)任务队列:存放等待执行的任务(BlockingQueue)
 3)最大线程:任务爆了之后扩容到的上限
 4)空闲时间:非核心线程空闲多久就回收
 5)拒绝策略:队列 + 线程都满了,怎么拒绝新任务

2. 工作执行流程(重中之重)
 1)来了任务 → 先看核心线程数,没满就新建核心线程执行
 2)核心线程满了 → 进任务队列排队
 3)队列也满了 → 新建非核心线程,直到达到最大线程数
 4)最大线程也满了 + 队列也满了 → 触发拒绝策略 

背下来:核心线程 → 队列 → 最大线程 → 拒绝策略

8.3 ThreadPoolExecutor 7 大核心参数

public ThreadPoolExecutor(
    int corePoolSize,       // 1.核心线程数
    int maximumPoolSize,    // 2.最大线程数
    long keepAliveTime,     // 3.空闲线程存活时间
    TimeUnit unit,          // 4.时间单位
    BlockingQueue<Runnable> workQueue, // 5.任务队列
    ThreadFactory threadFactory,       // 6.线程工厂
    RejectedExecutionHandler handler  // 7.拒绝策略
)

详细讲解核心参数:

1. corePoolSize 核心线程数
 1)常驻线程,不会被回收
 2)业务低峰也保留,随时处理任务
 3)实际配置:
   CPU 密集型:核心数 = CPU 核心数
   IO 密集型(接口、DB、网络):核心数设大一点 10~30
2. maximumPoolSize 最大线程数
 1)系统能承受的最大并发线程数
 2)超过这个数就不再建新线程
3. keepAliveTime 空闲存活时间
 1)非核心线程空闲多久就销毁
 2)避免空闲线程一直占资源
4. workQueue 任务阻塞队列
常用队列:
 1)ArrayBlockingQueue:有界队列,固定容量(工作最常用)
 2)LinkedBlockingQueue:无界 / 有界,不设容量就无限放任务 → 容易 OOM
 3)SynchronousQueue:不存任务,来了直接新建线程
 4)DelayQueue:延迟任务队列
工作禁忌:不要用无界队列,请求暴涨直接内存溢出。
5. ThreadFactory 线程工厂
 1)自定义线程名称、优先级、是否守护线程
 2)工作必须自定义:方便日志排查、定位问题
6. RejectedExecutionHandler 拒绝策略(4 种)
 1)AbortPolicy:直接抛异常(默认)
 2)CallerRunsPolicy:谁提交任务谁自己跑(主线程执行)
 3)DiscardPolicy:直接丢弃任务,不报错
 4)DiscardOldestPolicy:丢弃队列最老任务,执行新任务
工作场景:
1)支付、订单:用 AbortPolicy 抛异常告警
2)日志、统计:用 DiscardPolicy 丢弃无关紧要任务
3)非核心业务:CallerRunsPolicy 降级自己执行

8.4 JDK 内置 4 种线程池(工作禁止直接用,面试要懂)

1. Executors.newFixedThreadPool(n)
固定核心线程 = 最大线程
队列无界 → 高并发 OOM
2. Executors.newCachedThreadPool()
无核心线程,最大无限
任务多就无限建线程 → CPU 打爆
3. Executors.newSingleThreadExecutor()
单线程,串行执行
适合顺序任务,但无界队列有风险
4. Executors.newScheduledThreadPool()
定时、周期性任务
阿里开发规范:禁止使用 Executors 创建线程池,必须手动 new ThreadPoolExecutor 7 个参数自己配置
原因:内置线程池要么无界队列、要么无限线程,生产容易事故。

8.5 实际工作中怎么手动创建线程池(标准模板)

import java.util.concurrent.*;

public class ThreadPoolConfig {

    // 自定义线程工厂
    private static final ThreadFactory NAMED_THREAD_FACTORY = 
        new ThreadFactoryBuilder()
                .setNameFormat("biz-pool-%d")
                .setDaemon(false)
                .build();

    // 业务线程池 全局单例
    public static final ThreadPoolExecutor BIZ_POOL = new ThreadPoolExecutor(
            10,                     // 核心线程
            30,                     // 最大线程
            60L,                    // 空闲存活60秒
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100), // 有界队列
            NAMED_THREAD_FACTORY,
            new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
    );
}

工作用法:全局一个线程池,各处复用提交任务。

8.6 线程池提交任务的两种方式

  1. execute 无返回值
BIZ_POOL.execute(() -> {
    // 异步业务:记录日志、推送消息
});
  1. submit 有返回值(Callable + Future)
Future<Integer> future = BIZ_POOL.submit(() -> {
    return 100 + 200;
});
// 阻塞获取结果
Integer res = future.get();

适用:需要异步计算、拿到结果的业务。

8.7 实际工作常用业务场景(全覆盖)

场景 1:接口异步解耦

用户下单:

主线程:落库、返回订单号

线程池异步:发短信、发推送、记录日志、积分发放

提升接口响应速度

场景 2:批量任务处理

批量导入、批量对账、批量数据同步

用线程池多线程分片处理,提速

场景 3:限流保护

限制最大线程数,防止瞬间流量打垮 DB / 接口

场景 4:定时任务

用 ScheduledThreadPool 做延迟任务、周期巡检

场景 5:多线程并行计算

大数据统计、报表生成,多线程拆分计算再汇总

8.8 线程池常见坑(工作中容易踩的坑)

1、用无界队列 → 流量暴涨 OOM
2、不自定义线程名 → 线上故障无法排查堆栈
3、不配置拒绝策略 → 默认抛异常无告警
4、线程池不全局复用 → 到处新建线程池,资源浪费
5、get () 不设超时 → 无限阻塞卡死业务
6、任务内部不捕获异常 → 线程挂掉,线程池少线程

8.9 线程池核心知识点 面试极简背诵版

1、线程池优势:线程复用、控制并发、资源管控、异步解耦
2、执行流程:核心线程 → 队列 → 最大线程 → 拒绝策略
3、7 大参数:核心、最大、空闲时间、单位、队列、线程工厂、拒绝策略
4、4 种拒绝策略:中止、调用者运行、丢弃、丢弃最旧
5、工作规范:不用 Executors,手动 new ThreadPoolExecutor,有界队列、自定义线程名
6、IO 密集型线程数配大,CPU 密集型配接近核心数

8.10 实例演示:

// 实际工作中建议手动创建线程池
public class ThreadPoolConfig {
    public ExecutorService createThreadPool() {
        int corePoolSize = 5;      // 核心线程数
        int maxPoolSize = 20;      // 最大线程数
        long keepAliveTime = 60L;  // 空闲线程存活时间
        int queueCapacity = 100;    // 队列容量
        
        return new ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            keepAliveTime,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(queueCapacity),
            new CustomThreadFactory(),  // 自定义线程工厂
            new CustomRejectedHandler() // 拒绝策略
        );
    }
}

// 自定义线程工厂
class CustomThreadFactory implements ThreadFactory {
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, "custom-pool-" + threadNumber.getAndIncrement());
        thread.setDaemon(false);
        thread.setPriority(Thread.NORM_PRIORITY);
        return thread;
    }
}

// 自定义拒绝策略
class CustomRejectedHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 记录日志
        System.err.println("任务被拒绝:" + r);
        // 可选的降级策略
        if (!executor.isShutdown()) {
            r.run();  // 由调用线程执行
        }
    }
}

九、并发集合

9.1 ConcurrentHashMap

1. ConcurrentHashMap 是什么?
这是一个线程安全的本地缓存工具类
用 ConcurrentHashMap 实现,高并发下绝对安全,用来做:
1)本地缓存
2)共享配置
3)热点数据存储
4)多线程读写安全的 KV 存储

2. ConcurrentHashMap 原理?
ConcurrentHashMap = 线程安全的高效哈希表
它解决了 2 个问题:
1)HashMap 多线程不安全(会死循环、数据丢失)
2)Hashtable 效率极低(整张表加锁,并发差)
ConcurrentHashMap 原理(简单讲)
1)分段锁 / 桶锁(JDK 1.7 分段,JDK 1.8+ CAS + synchronized)
2)只锁一个哈希桶,不锁整张表
3)读不加锁,写轻量锁
4)高并发下性能极高

3. 这个工具类在实际工作中怎么用?(真实业务场景)
场景 1:本地缓存(最常用)
ConcurrentCache<String, User> userCache = new ConcurrentCache<>();
// 高并发下安全获取用户
User user = userCache.getOrCreate(userId, id -> userService.getById(id));

场景 2:多线程共享计数器 / 配置
ConcurrentCache<String, Integer> config = new ConcurrentCache<>();

场景 3:接口限流、缓存标记、分布式锁标记
cache.getOrCreate("limit:user:1001", key -> 1);

场景 4:全局临时缓存
比如:
  商品详情缓存
  首页热点数据
  高频访问的配置

4. ConcurrentHashMap 最关键特点
1)线程安全
 多线程读写不会出错
2)高性能
 读操作无锁
 写操作只锁一个节点
 并发能力远超 Hashtable
3)key 和 value 都不能为 null
 这是它和 HashMap 的重要区别
4. 常用原子方法(工作必用)
// 不存在才 put(原子)
map.putIfAbsent(key, value);
// 不存在则创建(原子,最常用)
map.computeIfAbsent(key, k -> createValue(k));
// 修改值(原子)
map.compute(key, (k, v) -> v + 1);
// 替换(原子)
map.replace(key, oldValue, newValue);

工作中最标准、最常用、最稳定的【线程安全本地缓存】写法如下:

public class ConcurrentCache<K, V> {
    private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
    
    // 原子获取/创建(缓存最常用)
    public V getOrCreate(K key, Function<K, V> creator) {
        return cache.computeIfAbsent(key, creator);
    }
    
    // 线程安全批量更新
    public void batchUpdate(Map<K, V> updates) {
        cache.putAll(updates);
    }
}

9.2 CopyOnWriteArrayList

1. 核心原理
核心原理:写时复制(Copy-On-Write)
规则只有一句话:
只要修改(add/set/remove),就复制一个新数组,操作新数组,最后替换引用。

1)写操作流程(add)
加锁(保证多线程写不乱)
复制原数组 → 创建一个更长的新数组
把新元素加到新数组
将引用指向新数组
释放锁
2)读操作流程(get / 遍历)
不加锁
直接读取当前引用指向的数组
速度 = 普通 ArrayList 一样快

2. 优缺点
优点
1)读性能极高
2)遍历安全,不会抛异常
3)高并发下非常稳定
4)多线程读写可同时进行(读无锁、写锁)
缺点
1)写性能差
2)写时内存占用翻倍(复制数组)
3)数据不是强实时(读快照)
4)不能用于频繁修改的场景

3. 使用场景
必须满足:读多写少!
最典型场景:
1)事件监听器列表
2)白名单、黑名单
3)系统配置列表(很少变,经常读)
4)本地缓存的集合
5)高并发遍历、不能加锁的场景

经典代码(时间监听器列表)

// 实际场景:读多写少的场景
public class EventManager {
    private final CopyOnWriteArrayList<EventListener> listeners = 
        new CopyOnWriteArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(listener);  // 写时复制
    }
    
    public void fireEvent(Event event) {
        for (EventListener listener : listeners) {  // 快照迭代
            listener.onEvent(event);
        }
    }
}

9.3 BlockingQueue

1. 什么是BlockingQueue?
BlockingQueue = 线程安全的阻塞队列
是「生产者 - 消费者模型」的官方最佳实现!
核心规则:
1)队列满了 → 再往里放的线程会阻塞(等待)
2)队列空了 → 再往外拿的线程会阻塞(等待)
3)自带线程阻塞、唤醒能力,不用自己写 wait/notify

2. 生活中的比喻(秒懂)
BlockingQueue 就是一个排队通道:
1)生产者 = 往通道放货物的人
2)消费者 = 从通道拿货物的人
3)通道满了 → 生产者必须等
4)通道空了 → 消费者必须等
5)JVM 自动帮你阻塞、唤醒

3. 常用 5 大 BlockingQueue 实现类(工作 + 面试)
1)ArrayBlockingQueue(最常用!有界队列)
 数组实现
 有界(必须指定容量)
 公平 / 非公平可选
 生产环境首选!
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
2)LinkedBlockingQueue(无界 / 有界链表)
 链表实现
 如果不指定容量 = 无限大小(可能 OOM)
 吞吐量比 Array 高
 慎用无界!
3)SynchronousQueue(不存储元素的队列)
 容量 = 0
 放一个必须等另一个取走
 适合任务量大、直接交换
 Executors.newCachedThreadPool () 底层用它
4)PriorityBlockingQueue(优先级队列)
按优先级出队,不是 FIFO
无界
5) DelayQueue(延迟队列)
到期才能取出
适合:订单超时取消、任务延迟执行

4. BlockingQueue 最核心特点
1)线程安全(底层用 ReentrantLock)
2)自动阻塞、自动唤醒
3)有界队列可以防止无限任务导致 OOM
4)高并发、高性能
5)是线程池、MQ、RPC 框架的底层核心组件

5. BlockingQueue 面试必背总结
1)BlockingQueue 是线程安全的阻塞队列
2)满了 put 阻塞,空了 take 阻塞
3)常用:ArrayBlockingQueue(有界安全)
4)底层:Lock + Condition 实现等待唤醒
5)核心用途:生产者 - 消费者、线程池、削峰、异步解耦
6)生产必须用有界队列,避免 OOM

实例演示:

// 实际场景:生产者-消费者
public class DataProcessor {
    private final BlockingQueue<Data> queue = new LinkedBlockingQueue<>(100);
    
    public void start() {
        // 生产者
        new Thread(() -> {
            while (true) {
                Data data = produceData();
                try {
                    queue.put(data);  // 队列满时阻塞
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }).start();
        
        // 消费者
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                while (true) {
                    try {
                        Data data = queue.take();  // 队列空时阻塞
                        processData(data);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }).start();
        }
    }
}

十、线程池监控与调优

10.1 监控指标

public class ThreadPoolMonitor {
    private final ThreadPoolExecutor executor;
    
    public void printStats() {
        System.out.println("核心线程数:" + executor.getCorePoolSize());
        System.out.println("活动线程数:" + executor.getActiveCount());
        System.out.println("最大线程数:" + executor.getMaximumPoolSize());
        System.out.println("队列大小:" + executor.getQueue().size());
        System.out.println("完成任务数:" + executor.getCompletedTaskCount());
    }
}

10.2 参数调优策略

public class ThreadPoolTuning {
    /**
     * CPU密集型任务
     * 核心线程数 = CPU核数 + 1
     */
    public ExecutorService cpuIntensivePool() {
        int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
        return new ThreadPoolExecutor(
            corePoolSize,
            corePoolSize * 2,
            60L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000)
        );
    }
    
    /**
     * IO密集型任务
     * 核心线程数 = CPU核数 * 2
     */
    public ExecutorService ioIntensivePool() {
        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
        return new ThreadPoolExecutor(
            corePoolSize,
            corePoolSize * 2,
            60L,
            TimeUnit.SECONDS,
            new SynchronousQueue<>()  // 直接交接,无队列
        );
    }
}

十一、实际工作最佳实践

11.1 异常处理

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 统一异常处理工具类
 * 处理:普通线程异常 + 线程池线程异常
 */
public class ThreadExceptionHandler {

    /**
     * 全局统一异常处理
     */
    public void handleThreadException() {
        // ====================== 1. 全局默认未捕获异常处理器(所有线程通用) ======================
        Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
            System.err.println("\n【全局线程异常捕获】");
            System.err.println("线程名称:" + thread.getName());
            System.err.println("线程ID:" + thread.getId());
            System.err.println("异常信息:" + throwable.getMessage());
            System.err.println("异常堆栈:");
            throwable.printStackTrace(); // 打印完整堆栈

            // 实际业务可在这里:
            // 1. 记录日志(log.error)
            // 2. 发送告警(邮件/短信/钉钉)
            // 3. 线程自动恢复
        });

        // ====================== 2. 线程池专用异常处理器 ======================
        ExecutorService executor = new ThreadPoolExecutor(
            5, 5, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(),
            // 自定义线程工厂,给池内每个线程设置异常处理器
            new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(r);
                    t.setName("thread-pool-" + t.getId());
                    
                    // 给线程池线程单独设置异常处理器
                    t.setUncaughtExceptionHandler((poolThread, throwable) -> {
                        System.err.println("\n【线程池线程异常捕获】");
                        System.err.println("线程名称:" + poolThread.getName());
                        System.err.println("异常信息:" + throwable.getMessage());
                        throwable.printStackTrace();
                    });
                    
                    return t;
                }
            }
        );
    }
}

11.2 资源清理

// 确保线程池正确关闭
public class ResourceCleanup {
    public void shutdownGracefully(ExecutorService executor) {
        executor.shutdown();  // 停止接收新任务
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();  // 取消正在执行的任务
                if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                    System.err.println("线程池未正常关闭");
                }
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

11.3 避免常见的一些陷阱

1、死锁预防:按固定顺序获取锁
2、活锁避免:加入随机退让
3、线程泄漏:确保线程正确关闭
4、上下文切换开销:避免创建过多线程
5、内存可见性:正确使用volatile/final
posted @ 2026-05-22 14:06  爱的万龙决  阅读(20)  评论(0)    收藏  举报