多线程并发篇(八股)

多线程并发篇

1.1 synchronized的实现原理?有哪些优化?

✅ 正确回答思路:

synchronized是Java最基本的同步机制,我从使用方式、底层原理、锁优化三个方面详细说明。

一、synchronized的三种使用方式

java 复制代码
// 1. 修饰实例方法(锁的是this)
public synchronized void method1() {
    // 临界区
}

// 2. 修饰静态方法(锁的是Class对象)
public static synchronized void method2() {
    // 临界区
}

// 3. 修饰代码块(锁的是指定对象)
public void method3() {
    synchronized (this) {  // 或者其他对象
        // 临界区
    }
}

二、synchronized的底层实现(JVM层面)

1. 同步代码块:monitorenter和monitorexit指令

java 复制代码
public void method() {
    synchronized (this) {
        System.out.println("hello");
    }
}

// 编译后的字节码:
0: aload_0
1: dup
2: astore_1
3: monitorenter        ← 进入同步块
4: getstatic
7: ldc
9: invokevirtual
12: aload_1
13: monitorexit        ← 正常退出
14: goto 22
17: astore_2
18: aload_1
19: monitorexit        ← 异常退出
20: aload_2
21: athrow
22: return

2. 同步方法:ACC_SYNCHRONIZED标志

java 复制代码
public synchronized void method() {
    System.out.println("hello");
}

// 方法标志:
flags: ACC_PUBLIC, ACC_SYNCHRONIZED  ← 有这个标志

三、对象头和Monitor(核心原理)

Java对象在内存中的结构:

复制代码
对象 = 对象头 + 实例数据 + 对齐填充

对象头 = Mark Word + Class Pointer(类型指针)

Mark Word(重点!):

在32位JVM中,Mark Word占32bit,存储对象的哈希码、GC年龄、锁信息等。

复制代码
无锁状态:
|----------------------------------------|
| hashCode(25bit) | GC年龄(4bit) | 0 01 |
|----------------------------------------|

偏向锁:
|----------------------------------------|
| 线程ID(23bit) | Epoch(2) | GC(4) | 1 01 |
|----------------------------------------|

轻量级锁:
|----------------------------------------|
| 指向栈中锁记录的指针(30bit) | 00 |
|----------------------------------------|

重量级锁:
|----------------------------------------|
| 指向Monitor对象的指针(30bit) | 10 |
|----------------------------------------|

GC标记:
|----------------------------------------|
| 空 | 11 |
|----------------------------------------|

最后2bit是锁标志位

  • 01:无锁或偏向锁
  • 00:轻量级锁
  • 10:重量级锁
  • 11:GC标记

Monitor对象(重量级锁):

复制代码
每个对象都关联一个Monitor对象

Monitor {
    _owner: 持有锁的线程
    _EntryList: 等待获取锁的线程队列(阻塞队列)
    _WaitSet: 调用wait()的线程队列
    _count: 重入次数
}

加锁流程:
1. 线程尝试获取Monitor
2. 获取成功:_owner指向该线程,_count++
3. 获取失败:进入_EntryList阻塞等待
4. 释放锁:_count--,为0时唤醒_EntryList中的线程

四、synchronized的锁升级(JDK 1.6优化,面试高频!)

JDK 1.6之前,synchronized就是重量级锁,性能差。JDK 1.6引入了锁升级机制:

复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁

单向升级,不可降级!(除非GC)

1. 偏向锁(Biased Locking)

场景:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。

原理

复制代码
1. 第一次获取锁时,在对象头Mark Word中记录线程ID
2. 之后该线程再次获取锁时,检查线程ID是否是自己
3. 是:直接获取锁,不需要CAS操作
4. 不是:撤销偏向锁,升级为轻量级锁

为什么叫偏向? 因为锁"偏向"于第一个获取它的线程。

偏向锁的撤销

复制代码
当有其他线程尝试获取偏向锁时:
1. 等待全局安全点(STW)
2. 检查持有偏向锁的线程是否还活着
   - 不活着:对象头设为无锁状态
   - 活着:
     - 线程不在同步块中:撤销偏向,设为无锁
     - 线程在同步块中:升级为轻量级锁

开启/关闭偏向锁:

bash 复制代码
# JDK 6/7 默认开启偏向锁
# 关闭偏向锁
-XX:-UseBiasedLocking

2. 轻量级锁(Lightweight Locking)

场景:多线程交替执行同步块,不存在竞争(线程A用完,线程B才用)。

原理

复制代码
加锁流程:
1. 在当前线程的栈帧中创建锁记录(Lock Record)
2. 复制对象头的Mark Word到锁记录中
3. 用CAS操作尝试把对象头的Mark Word替换为指向锁记录的指针
4. CAS成功:获取轻量级锁
5. CAS失败:
   - 检查对象头是否指向当前线程的栈帧(重入)
   - 是:直接获取锁
   - 不是:锁膨胀为重量级锁

解锁流程:
1. 用CAS操作把锁记录中的Mark Word替换回对象头
2. CAS成功:释放锁
3. CAS失败:说明有竞争,膨胀为重量级锁,唤醒等待线程

自旋优化

轻量级锁如果CAS失败,不会立即膨胀为重量级锁,而是自旋(循环尝试获取锁)。

java 复制代码
// 自旋伪代码
int spinCount = 0;
while (spinCount < MAX_SPIN_COUNT) {
    if (CAS成功) {
        获取锁;
        break;
    }
    spinCount++;
}

if (spinCount >= MAX_SPIN_COUNT) {
    膨胀为重量级锁;
}

自适应自旋(JDK 1.6):

复制代码
不再固定自旋次数,而是动态调整:
- 如果上次自旋成功了,这次可以多自旋几次
- 如果上次自旋失败了,这次少自旋或不自旋

3. 重量级锁(Heavyweight Locking)

场景:多线程同时竞争锁。

原理

复制代码
1. 膨胀为重量级锁(在对象头中指向Monitor对象)
2. 竞争失败的线程进入Monitor的_EntryList阻塞
3. 持有锁的线程释放锁后,唤醒_EntryList中的线程
4. 被唤醒的线程竞争锁

重量级锁的代价

  • 线程阻塞/唤醒需要CPU从用户态切换到内核态
  • 切换开销大

五、锁升级的完整流程图

复制代码
线程A第一次获取锁
    ↓
无锁 → 偏向锁(Mark Word记录线程A的ID)
    ↓
线程A再次获取锁:检查线程ID,是自己,直接获取
    ↓
线程B尝试获取锁
    ↓
线程A还持有锁?
    ├─ 是:升级为轻量级锁,线程B自旋等待
    │      ↓
    │   自旋成功?
    │      ├─ 是:线程B获取锁
    │      └─ 否:升级为重量级锁,线程B阻塞
    │
    └─ 否:撤销偏向锁,设为无锁或轻量级锁

六、synchronized的其他优化

1. 锁粗化(Lock Coarsening)

java 复制代码
// ❌ 锁粒度太细
for (int i = 0; i < 100; i++) {
    synchronized (this) {
        // 操作
    }
}

// ✅ JVM优化后:锁粗化
synchronized (this) {
    for (int i = 0; i < 100; i++) {
        // 操作
    }
}

2. 锁消除(Lock Elimination)

java 复制代码
public String concatString(String s1, String s2) {
    StringBuffer sb = new StringBuffer();  // 局部变量,不会逃逸
    sb.append(s1);  // StringBuffer的append是synchronized的
    sb.append(s2);
    return sb.toString();
}

// JVM优化:检测到sb不会逃逸出方法,消除StringBuffer内部的synchronized

七、synchronized vs Lock

对比项 synchronized ReentrantLock
使用 关键字,自动加锁解锁 类,手动lock/unlock
灵活性 不灵活 灵活(可中断、超时、公平锁)
性能 JDK 1.6后和Lock差不多 稍高(JDK 1.6之前明显高)
可重入 支持 支持
锁释放 自动(异常也能释放) 手动(需要finally)
条件变量 1个(wait/notify) 多个(Condition)
推荐 优先使用 需要高级功能时使用

八、实际项目经验:

"我们项目中优先使用synchronized,因为JDK 1.8环境下,synchronized的性能已经很好了,而且使用简单,不容易出错。只有在需要tryLock()、可中断锁、公平锁等高级功能时,才用ReentrantLock。"

💡 总结:

  • synchronized通过Monitor对象实现同步
  • JDK 1.6引入了偏向锁、轻量级锁、自旋锁等优化
  • 锁会单向升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  • 现代JVM中,synchronized性能已经很好,优先使用

1.2 volatile的作用是什么?如何保证可见性和有序性?

✅ 正确回答思路:

volatile是Java提供的轻量级同步机制,我从作用、底层实现、使用场景三个方面来答。

一、volatile的两大作用

1. 保证可见性(Visibility) 2. 禁止指令重排序(Ordering)

注意 :volatile不保证原子性

二、什么是可见性问题?

Java内存模型(JMM):

复制代码
CPU1                CPU2                CPU3
 ↓                   ↓                   ↓
工作内存1            工作内存2            工作内存3
 ↓                   ↓                   ↓
|---------------主内存(共享)---------------|

可见性问题示例:

java 复制代码
public class VisibilityTest {
    private static boolean flag = false;
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!flag) {
                // 线程1一直循环,等待flag变为true
            }
            System.out.println("线程1结束");
        }).start();
        
        Thread.sleep(1000);
        
        flag = true;  // 主线程修改flag
        System.out.println("主线程修改flag为true");
    }
}

// 可能的结果:线程1永远不会结束!
// 原因:线程1把flag读到自己的工作内存中,一直用工作内存中的值(false)
//      主线程修改了主内存中的flag,但线程1看不到

加上volatile解决:

java 复制代码
private static volatile boolean flag = false;  // ← 加上volatile

// 现在线程1能看到主线程的修改,会正常结束

volatile如何保证可见性?

底层实现 :通过内存屏障(Memory Barrier)

复制代码
1. 写volatile变量时:
   - 在写操作之后插入一个Store Barrier
   - 作用:强制把CPU缓存中的数据刷新到主内存
   
2. 读volatile变量时:
   - 在读操作之前插入一个Load Barrier
   - 作用:强制从主内存读取最新值,而不是用CPU缓存

具体实现(x86架构):
- 会在写volatile变量的指令后面加上一个 lock 前缀指令
- lock 前缀指令会:
  1. 锁定缓存行
  2. 把修改写回主内存
  3. 使其他CPU的缓存行无效(MESI协议)

字节码层面:

java 复制代码
// volatile变量
private volatile int count = 0;

// 字节码中会有 ACC_VOLATILE 标志
private volatile int count;
  descriptor: I
  flags: ACC_VOLATILE  ← 这个标志

三、什么是指令重排序问题?

为了提高性能,编译器和CPU可能会对指令重排序:

java 复制代码
// 原始代码
int a = 1;  // 1
int b = 2;  // 2
int c = a + b;  // 3

// 可能重排序为:
int b = 2;  // 2
int a = 1;  // 1
int c = a + b;  // 3

// 只要保证单线程语义不变,就可以重排

多线程下的重排序问题(经典的双重检查锁定):

java 复制代码
public class Singleton {
    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. 把instance指向内存空间(此时对象还没初始化!)
2. 初始化对象

问题:
线程A执行到步骤3,instance != null了,但对象还没初始化
线程B判断instance != null,直接返回,使用了未初始化的对象!

加上volatile解决:

java 复制代码
private static volatile Singleton instance;  // ← 加上volatile

// volatile禁止 步骤2和步骤3 重排序,保证instance指向的一定是初始化完成的对象

volatile如何禁止重排序?

内存屏障(Memory Barrier):

JMM在volatile读写操作前后插入内存屏障:

复制代码
写volatile变量:
  StoreStore Barrier
  写volatile变量
  StoreLoad Barrier

读volatile变量:
  LoadLoad Barrier
  读volatile变量
  LoadStore Barrier

四种内存屏障:

复制代码
1. LoadLoad Barrier
   Load1; LoadLoad; Load2
   → 保证Load1的数据加载先于Load2

2. StoreStore Barrier
   Store1; StoreStore; Store2
   → 保证Store1的数据对其他处理器可见,先于Store2

3. LoadStore Barrier
   Load1; LoadStore; Store2
   → 保证Load1的数据加载先于Store2

4. StoreLoad Barrier(最重的屏障)
   Store1; StoreLoad; Load2
   → 保证Store1的数据对所有处理器可见,先于Load2

volatile的happens-before规则:

复制代码
对volatile变量的写 happens-before 对这个变量的读

意思是:
线程A写volatile变量之前的所有操作,对线程B读这个变量之后的所有操作可见

四、volatile不保证原子性

问题示例:

java 复制代码
public class VolatileAtomicTest {
    private volatile int count = 0;
    
    public void increment() {
        count++;  // 不是原子操作!
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileAtomicTest test = new VolatileAtomicTest();
        
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increment();
                }
            });
            threads[i].start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        System.out.println(test.count);  // 期望10000,实际<10000
    }
}

为什么count++不是原子操作?

复制代码
count++ 分三步:
1. 读取count的值到工作内存
2. 在工作内存中+1
3. 写回主内存

虽然volatile保证了可见性,但不能保证这三步的原子性:

线程A读取count=0
线程B读取count=0
线程A计算count=1
线程B计算count=1
线程A写回count=1
线程B写回count=1

结果:两次++,count只增加了1

解决办法:

java 复制代码
// 方案1:synchronized
public synchronized void increment() {
    count++;
}

// 方案2:AtomicInteger
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
    count.incrementAndGet();  // 原子操作
}

// 方案3:Lock
private Lock lock = new ReentrantLock();

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

五、volatile的使用场景

场景1:状态标志

java 复制代码
private volatile boolean shutdown = false;

public void shutdown() {
    shutdown = true;
}

public void doWork() {
    while (!shutdown) {
        // 工作
    }
}

场景2:双重检查锁定(DCL)

java 复制代码
private volatile Helper helper;

public Helper getHelper() {
    if (helper == null) {
        synchronized(this) {
            if (helper == null) {
                helper = new Helper();
            }
        }
    }
    return helper;
}

场景3:一次性安全发布

java 复制代码
private volatile int value;

public void setValue(int value) {
    this.value = value;
}

public int getValue() {
    return value;
}

不适合的场景:

java 复制代码
// ❌ 不适合:需要原子性
private volatile int count = 0;
public void increment() {
    count++;  // 不是原子操作
}

// ❌ 不适合:依赖当前值
public void setFlag() {
    flag = !flag;  // 读-改-写,不是原子操作
}

六、volatile vs synchronized

对比项 volatile synchronized
作用对象 变量 方法或代码块
原子性 不保证 保证
可见性 保证 保证
有序性 部分保证(禁止重排) 保证
性能 高(轻量级) 较低(JDK 1.6后优化)
阻塞 不阻塞 可能阻塞

七、实际项目经验:

"我们项目中用volatile的地方主要是双重检查锁定的单例模式,还有一些状态标志位。对于需要原子性的操作,比如计数器,我们用AtomicInteger而不是volatile int。"

💡 总结:

  • volatile通过内存屏障保证可见性和禁止重排序
  • volatile不保证原子性
  • 适用于状态标志、DCL、一次性安全发布
  • 需要原子性时用synchronized或Atomic类

1.3 线程池的核心参数有哪些?线程池的工作流程是什么?

✅ 正确回答思路:

线程池是Java并发编程的核心工具,我从参数、工作流程、拒绝策略、实际使用四个方面详细说明。

一、ThreadPoolExecutor的7大核心参数

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,              // 核心线程数
    int maximumPoolSize,           // 最大线程数
    long keepAliveTime,            // 非核心线程空闲存活时间
    TimeUnit unit,                 // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,   // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)

详细说明每个参数:

1. corePoolSize(核心线程数)

复制代码
线程池的基本大小
- 即使线程空闲,也不会被回收(除非设置allowCoreThreadTimeOut)
- 新任务到来时,如果线程数<corePoolSize,会创建新线程

2. maximumPoolSize(最大线程数)

复制代码
线程池允许创建的最大线程数
- 线程数 = 核心线程 + 非核心线程
- 当任务队列满了,且线程数<maxPoolSize时,会创建非核心线程

3. keepAliveTime + unit(非核心线程存活时间)

复制代码
非核心线程空闲多久后被回收
- 当线程数>corePoolSize时,多余的空闲线程等待时间超过keepAliveTime就会被销毁
- 如果设置allowCoreThreadTimeOut(true),核心线程也会被回收

4. workQueue(任务队列)

复制代码
用于保存等待执行的任务

常用的队列类型:
1. ArrayBlockingQueue
   - 有界队列,基于数组
   - 需要指定容量
   
2. LinkedBlockingQueue
   - 可选有界队列,基于链表
   - 不指定容量时,默认Integer.MAX_VALUE(相当于无界)
   
3. SynchronousQueue
   - 不存储元素的阻塞队列
   - 每个插入操作必须等待一个移除操作
   - 适合任务数量不确定的场景
   
4. PriorityBlockingQueue
   - 优先级队列
   - 按优先级顺序执行任务
   
5. DelayQueue
   - 延迟队列
   - 元素到达延迟时间后才能被取出

5. threadFactory(线程工厂)

复制代码
用于创建新线程

作用:
- 可以自定义线程名称、优先级、是否守护线程等
- 方便问题排查

示例:
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
    .setNameFormat("my-pool-%d")
    .build();

6. handler(拒绝策略)

复制代码
当任务队列满了,且线程数达到maximumPoolSize时,如何处理新任务

JDK提供了4种拒绝策略:

1. AbortPolicy(默认)
   - 抛出RejectedExecutionException异常
   
2. CallerRunsPolicy
   - 由调用线程自己执行任务(不丢弃任务,但会降低新任务提交速度)
   
3. DiscardPolicy
   - 静默丢弃任务,不抛异常
   
4. DiscardOldestPolicy
   - 丢弃队列中最老的任务,然后尝试提交当前任务

二、线程池的工作流程(面试必考!)

复制代码
1. 提交任务到线程池

2. 判断核心线程数是否已满?
   ├─ 否:创建核心线程执行任务
   └─ 是:进入下一步

3. 判断任务队列是否已满?
   ├─ 否:任务加入队列等待
   └─ 是:进入下一步

4. 判断最大线程数是否已满?
   ├─ 否:创建非核心线程执行任务
   └─ 是:执行拒绝策略

图示(文字版):

复制代码
新任务到来
    ↓
线程数 < corePoolSize?
    ├─ 是 → 创建核心线程执行
    └─ 否 ↓
队列未满?
    ├─ 是 → 加入队列
    └─ 否 ↓
线程数 < maximumPoolSize?
    ├─ 是 → 创建非核心线程执行
    └─ 否 → 执行拒绝策略

举例说明:

java 复制代码
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2,  // corePoolSize
    5,  // maximumPoolSize
    60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10)  // 队列容量10
);

// 提交任务的流程:
// 第1个任务:线程数0 < core2,创建核心线程1执行
// 第2个任务:线程数1 < core2,创建核心线程2执行
// 第3个任务:核心线程满了,加入队列,队列1/10
// ...
// 第12个任务:核心线程满了,加入队列,队列10/10(队列满了!)
// 第13个任务:队列满了,线程数2 < max5,创建非核心线程3执行
// 第14个任务:队列满了,线程数3 < max5,创建非核心线程4执行
// 第15个任务:队列满了,线程数4 < max5,创建非核心线程5执行
// 第16个任务:队列满了,线程数5 = max5,执行拒绝策略!

三、常见的线程池类型(Executors工厂方法)

java 复制代码
// 1. newFixedThreadPool:固定线程数
ExecutorService pool = Executors.newFixedThreadPool(10);
// 等价于:
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

// 2. newCachedThreadPool:可缓存线程池
ExecutorService pool = Executors.newCachedThreadPool();
// 等价于:
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

// 3. newSingleThreadExecutor:单线程
ExecutorService pool = Executors.newSingleThreadExecutor();
// 等价于:
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

// 4. newScheduledThreadPool:定时任务线程池
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);

为什么不推荐用Executors创建线程池?(阿里巴巴Java开发手册强制规定)

复制代码
原因:

1. FixedThreadPool 和 SingleThreadExecutor:
   - 使用LinkedBlockingQueue,默认容量Integer.MAX_VALUE
   - 可能堆积大量请求,导致OOM
   
2. CachedThreadPool 和 ScheduledThreadPool:
   - maximumPoolSize是Integer.MAX_VALUE
   - 可能创建大量线程,导致OOM
   
推荐:手动创建ThreadPoolExecutor,明确指定参数

四、如何合理设置线程池参数?

CPU密集型任务

复制代码
线程数 = CPU核心数 + 1

原因:
- CPU密集型任务,线程一直在运行,不会阻塞
- CPU核心数+1,是为了当某个线程因为页缺失或其他原因阻塞时,额外的线程可以顶上

示例:
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    processors + 1,
    processors + 1,
    ...
);

IO密集型任务

复制代码
线程数 = CPU核心数 * 2
或者
线程数 = CPU核心数 / (1 - 阻塞系数)  // 阻塞系数一般0.8-0.9

原因:
- IO密集型任务,线程大部分时间在等待IO(网络、磁盘),CPU利用率低
- 可以创建更多线程,提高CPU利用率

示例:
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    processors * 2,
    processors * 2,
    ...
);

实际项目中的调优方法

复制代码
1. 压测
   - 模拟真实流量,观察CPU、内存、响应时间
   - 逐步调整线程数,找到最优值
   
2. 监控
   - 监控线程池的activeCount、queueSize、rejectedCount
   - 根据监控数据调整
   
3. 动态调整
   - 线程池支持动态修改参数
   - 可以通过配置中心动态调整

五、实际项目经验:

"我们项目用了自定义的ThreadPoolExecutor:

java 复制代码
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    10,  // 核心线程数:根据业务量压测得出
    20,  // 最大线程数:高峰期可以扩展到20
    60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),  // 有界队列,防止OOM
    new ThreadFactoryBuilder()
        .setNameFormat("business-pool-%d")
        .build(),
    new ThreadPoolExecutor.CallerRunsPolicy()  // 让调用线程执行,降低提交速度
);

// 监控
pool.submit(() -> {
    log.info("活跃线程数: {}", pool.getActiveCount());
    log.info("队列任务数: {}", pool.getQueue().size());
    log.info("已完成任务数: {}", pool.getCompletedTaskCount());
});

遇到过一次拒绝策略触发导致任务丢失的问题,排查后发现是队列容量设置太小了,后来调大到500,问题解决。"

💡 总结:

  • 线程池7大参数:核心线程数、最大线程数、存活时间、队列、线程工厂、拒绝策略
  • 工作流程:核心线程 → 队列 → 非核心线程 → 拒绝策略
  • 不推荐用Executors创建线程池,手动创建更可控
  • CPU密集型: CPU核心数+1,IO密集型: CPU核心数*2

相关推荐
Desirediscipline1 小时前
#define _CRT_SECURE_NO_WARNINGS 1
开发语言·数据结构·c++·算法·c#·github·visual studio
尘缘浮梦1 小时前
协程asyncio入门案例 1
开发语言·python
没有bug.的程序员1 小时前
Lombok 深度进阶:编译期增强内核、@Data 与 @Builder 逻辑博弈及工业级避坑实战指南
java·开发语言·python·builder·lombok·data·编译器增强
guygg881 小时前
基于人工神经网络的彩色图像恢复 MATLAB实现
开发语言·计算机视觉·matlab
m0_531237172 小时前
C语言-分支与循环语句练习2
c语言·开发语言·算法
懒惰成性的2 小时前
Java方法的使用
java·开发语言
蚊子码农3 小时前
算法题解记录-2452距离字典两次编辑以内的单词
开发语言·算法·c#
wangbing11253 小时前
Java构造函数不能加void
java·开发语言
Never_Satisfied3 小时前
在JavaScript / HTML中,数组查找第一个符合要求元素
开发语言·javascript·html