北京JAVA基础面试30天打卡02
1. Java 线程池的原理**
原理概览:
Java 线程池的核心类是 java.util.concurrent.ThreadPoolExecutor
,其设计理念是线程复用,避免频繁创建/销毁线程带来的性能开销。
核心组成参数:
java复制编辑
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, // 核心线程数maximumPoolSize, // 最大线程数keepAliveTime, // 非核心线程最大空闲时间unit, // 时间单位workQueue, // 阻塞队列threadFactory, // 线程工厂handler // 拒绝策略
);
工作流程:
- 线程数 < corePoolSize → 创建新线程处理任务。
- corePoolSize <= 线程数 < maximumPoolSize:
- 如果工作队列未满 → 放入队列;
- 如果工作队列已满 → 创建新线程。
- 当前线程数已达 maximumPoolSize 且队列也满 → 拒绝策略生效。
常用队列类型:
ArrayBlockingQueue
:有界 FIFO 队列。LinkedBlockingQueue
:可选容量,适合任务频率高场景。SynchronousQueue
:不存储元素,适合任务交付速度快的场景。
常用拒绝策略(RejectedExecutionHandler):
AbortPolicy
(默认):抛出异常。CallerRunsPolicy
:调用者线程执行任务。DiscardPolicy
:直接丢弃任务。DiscardOldestPolicy
:丢弃队列头部任务。
✅ 常用队列类型(用于线程池的构造函数中)
1.1. ArrayBlockingQueue
-
特点:有界队列,使用数组实现,FIFO 先进先出。
-
适合场景:任务量可控时(避免OOM),常用于固定大小线程池。
-
构造示例:
java复制编辑 new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));
1.2. LinkedBlockingQueue
-
特点:默认是无界队列(但也可以指定容量),使用链表实现,FIFO。
-
适合场景:任务量大,但允许任务排队;注意可能导致内存暴涨。
-
线程池行为:当任务提交速率 > 线程处理速率,会积压任务。
-
构造示例:
java复制编辑 new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
1.3. SynchronousQueue
-
特点:无容量的队列,提交任务必须有线程立即执行,否则无法加入队列。
-
适合场景:任务执行必须及时处理的情况(如
CachedThreadPool
)。 -
线程池行为:每来一个任务,就必须创建一个线程来处理。
-
构造示例:
java复制编辑 new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<>());
1.4. PriorityBlockingQueue
-
特点:支持任务优先级排序的无界阻塞队列(需要任务实现
Comparable
)。 -
适合场景:任务有优先级要求的线程池。
-
注意:线程池中不会因为高优先级任务而打断当前正在执行的低优先级任务。
-
构造示例:
java复制编辑 new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS,new PriorityBlockingQueue<>());
1.5. DelayQueue
(不常用于线程池)
- 特点:只有到达延迟时间的任务才会被消费。
- 适合场景:延迟执行任务场景,比如定时任务调度。
- ⚠️ 并不适合直接用在线程池中作为任务队列(任务不会立即取出)。
小结:线程池与队列的典型搭配
线程池类型 | 使用队列类型 | 特点说明 |
---|---|---|
FixedThreadPool | LinkedBlockingQueue | 固定线程数,无界队列,可能导致OOM |
CachedThreadPool | SynchronousQueue | 自动伸缩线程池、无任务排队 |
SingleThreadPool | LinkedBlockingQueue | 单线程顺序执行,保证顺序性 |
自定义线程池 | 自定义队列(如 ArrayBlockingQueue ) | 可控容量 + 可控线程数,最常用组合 |
额外:为什么线程池要先使用阻塞队列,而不是直接增加线程?
因为每创建一个线程都会占用一定的系统资源(如栈空间、线程调度开销等),直接增加线程会迅速消耗系统资源,导致性能下降。使用阻塞队列可以将任务暂存,避免线程数量无限增长,确保资源利用率更高。如果阻塞队列都满了,说明此时系统负载很大,再去增加线程到最大线程数去消化任务即可。举个例子:老板现在手下有10个人在干活(核心线程数),突然活变多了,每个人干不过来了,此时老板不会立马招人,它会让这些活积累一下(放到阻塞队列中),看看过段时间能不能消化掉。如果老板发现这个活积累的实在太多了(队列满了),他才会继续招人(达到最大线程数)。这就是所谓的人员(线程)有成本。
2. 使用过的 Java 并发工具类
Java 提供了丰富的并发工具类,位于 java.util.concurrent 包中。以下是我了解并常提到的并发工具类:
- Executor 框架
- ExecutorService、ThreadPoolExecutor:线程池管理任务。
- ScheduledExecutorService:支持定时和周期性任务。
- 并发集合
- ConcurrentHashMap:线程安全的哈希表,支持高并发读写。
- CopyOnWriteArrayList:适合读多写少的场景,写操作通过复制数组实现。
- BlockingQueue(如 ArrayBlockingQueue、LinkedBlockingQueue):线程安全的队列,常用于生产者-消费者模型。
- 锁机制
- ReentrantLock:可重入锁,支持公平/非公平锁,灵活性高于 synchronized。
- ReadWriteLock(如 ReentrantReadWriteLock):读写分离锁,适合读多写少场景。
- Condition:与 ReentrantLock 配合,实现线程间的精确唤醒。
- 同步工具
- CountDownLatch:允许线程等待直到计数器归零,常用于任务协调。
- CyclicBarrier:多个线程相互等待到达共同点后继续执行。
- Semaphore:控制同时访问资源的线程数。
- Phaser:动态调整的多阶段同步工具。
- 原子类
- AtomicInteger、AtomicLong、AtomicReference 等:基于 CAS 的无锁原子操作。
- LongAdder、DoubleAdder:高并发场景下优于 AtomicLong 的计数器。
- 其他
- ForkJoinPool:适用于递归分解任务的工作窃取线程池(如并行流)。
- CompletableFuture:异步编程工具,支持链式调用和回调。
这些工具类在不同场景下解决并发问题,比如线程同步、任务协调、资源
3. CAS (Compare-And-Swap) 操作
-
定义: CAS(Compare-And-Swap)是一种原子操作,用于实现无锁(lock-free)并发控制。它通过比较内存中的值与预期值是否一致,决定是否将新值写入内存。
-
工作原理
- CAS 操作有三个参数:内存位置(V)、旧的预期值(A)、新值(B)。
- 执行步骤:
- 读取内存位置 V 的当前值。
- 如果 V 的值等于 A(预期值),则将 V 更新为 B。
- 如果 V 的值不等于 A,操作失败,不修改 V。
- CAS 是原子性的,通常由硬件指令(如 CPU 的 cmpxchg)支持。
-
Java 中的实现
-
Java 通过 sun.misc.Unsafe 类提供 CAS 操作(如 compareAndSwapInt)。
-
高层封装在
java.util.concurrent.atomic
包中,例如:
- AtomicInteger 的 compareAndSet(int expect, int update)。
- AtomicReference 的 compareAndSet(V expect, V update)。
-
-
优点
- 无锁操作,避免锁的开销和死锁风险。
- 适合高并发场景,如计数器、原子更新。
-
缺点
- ABA 问题:如果值从 A 变为 B 再变回 A,CAS 可能误认为没有变化。解决方法:使用 AtomicStampedReference 带版本号。
- 自旋开销:CAS 失败时可能需要循环重试(自旋),消耗 CPU。
- 仅限单变量:CAS 无法直接处理多变量的原子操作。
-
典型应用
- AtomicInteger 的增减操作(如 incrementAndGet)。
- 并发集合(如 ConcurrentHashMap)的内部实现。
- 线程池的任务分配和状态更新。
补充
3.1. AtomicInteger 的增减操作(如 incrementAndGet)
AtomicInteger 是 Java java.util.concurrent.atomic 包中的类,基于 CAS 实现线程安全的原子操作。其增减操作(如 incrementAndGet)是 CAS 的经典应用。
-
核心方法:incrementAndGet 方法原子性地将值加 1 并返回新值,底层依赖 CAS。
-
实现流程
-
获取当前值(get() 方法读取 volatile 变量 value)。
-
计算新值(当前值 + 1)。
-
使用 CAS 操作( compareAndSet )尝试将当前值更新为新值:
- 如果当前值等于预期值(未被其他线程修改),更新成功,返回新值。
- 如果当前值不等于预期值(被其他线程修改),循环重试(自旋)。
-
-
代码示例(简化的 incrementAndGet逻辑):
public final int incrementAndGet() {for (;;) { // 自旋int current = get(); // 获取当前值int next = current + 1; // 计算新值if (compareAndSet(current, next)) // CAS 更新return next;}}
- compareAndSet 调用 sun.misc.Unsafe 的 compareAndSwapInt 方法,利用 CPU 原语保证原子性。
- value 是 volatile 类型,确保多线程间的可见性。
应用场景
- 计数器:如统计请求次数、任务完成数等,高并发场景下避免使用锁。
- ID 生成器:生成唯一的递增 ID。
- 状态标志:如控制开关的原子更新。
优势与注意事项
- 优势:无锁操作,性能优于 synchronized,适合高并发。
- 注意事项
- 高竞争下自旋可能导致 CPU 占用过多。
- 对于复杂计数场景(如高频增减),推荐使用 LongAdder,它通过分段计数降低 CAS 竞争。
3.2. ConcurrentHashMap 的内部实现
ConcurrentHashMap 是 Java 中线程安全的哈希表,广泛用于高并发场景。其内部实现大量使用 CAS 来保证线程安全,减少锁的使用。
-
数据结构
- JDK 8+ 的 ConcurrentHashMap 使用数组 + 链表/红黑树结构,类似 HashMap。
- 每个桶(bucket)是一个节点(Node 或 TreeNode),支持并发操作。
- CAS 在核心操作中的应用
-
put 操作
-
当向某个桶添加节点时,ConcurrentHashMap 使用 CAS 更新桶的头节点。
-
如果桶为空,尝试通过 CAS 将新节点设置为头节点(tabAt 和 casTabAt 方法)。
-
如果 CAS 失败(其他线程已修改),进入同步块(synchronized)或重试。
-
代码示例(简化的 putVal逻辑): java
if (casTabAt(tab, i, null, newNode)) { // CAS 设置桶头节点// 成功插入 } else {// 失败,进入同步逻辑或重试 }
-
-
扩容(resize)
- ConcurrentHashMap 支持并发扩容,多个线程协作转移元素。
- 使用 CAS 更新 sizeCtl(控制扩容状态的变量)和 transferIndex(表示扩容进度)。
- 每个线程通过 CAS 领取一段桶区间进行转移,避免冲突。
-
计数器(size)
- ConcurrentHashMap 使用 CounterCell 数组(类似 LongAdder)记录元素个数。
- 更新计数时,通过 CAS 修改 CounterCell 的值,避免全局锁。
关键点
- 分段优化:通过分桶和分段计数,降低 CAS 竞争。
- 锁 + CAS 结合:CAS 用于快速尝试更新,失败时回退到 synchronized 锁(仅锁定单个桶)。
- volatile + CAS:数组和节点的 volatile 字段确保可见性,CAS 保证原子性。
应用场景
- 缓存:如存储配置、会话数据,允许多线程并发读写。
- 任务分发:如线程池中存储任务状态。
- 数据共享:多线程共享的键值存储。
优势与注意事项
-
优势:高并发下读写性能优于 Hashtable 和 synchronized 的 HashMap。
-
注意事项
- CAS 失败可能导致短暂的 synchronized 阻塞。
- 内存占用较高(分段计数和红黑树)。
3.3. 线程池的任务分配和状态更新
Java 线程池(ThreadPoolExecutor)使用 CAS 管理任务分配和线程池状态(如运行、关闭等),确保线程安全且高效。
-
线程池状态管理
-
ThreadPoolExecutor 使用单个 AtomicInteger( ctl )存储线程池状态和线程数:
- 高 3 位表示状态(如 RUNNING、SHUTDOWN)。
- 低 29 位表示工作线程数。
-
使用 CAS 更新 ctl,如增加/减少线程数、切换状态。
-
示例:runState 的变更(如 tryTerminate 方法)通过 CAS 确保原子性。
-
-
任务分配
-
任务提交到任务队列(如 BlockingQueue)时,使用 CAS 确保入队操作的线程安全。
-
工作线程从队列获取任务时,依赖队列的内部 CAS(如 LinkedBlockingQueue 的 take 方法)。
-
示例( LinkedBlockingQueue的 enqueue):
-
if (casTail(tail, newNode)) { // CAS 更新尾节点// 入队成功}```
-
线程创建与销毁
- 创建新线程时,通过 CAS 增加 ctl 中的线程计数。
- 线程空闲回收时,通过 CAS 减少线程计数,确保计数准确。
具体实现
- ctl 的 CAS 操作
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));private boolean compareAndIncrementWorkerCount(int expect) {return ctl.compareAndSet(expect, expect + 1);}
-
用于在添加工作线程时原子性增加计数。
-
拒绝策略触发
- 当任务队列满且线程数达到最大时,CAS 失败会触发拒绝策略(如 RejectedExecutionException)。
应用场景
- 任务调度:线程池接收任务并分配给空闲线程,CAS 确保任务队列和线程计数一致。
- 状态控制:如关闭线程池(shutdown),通过 CAS 切换状态,防止并发修改。
- 动态调整:如动态调整核心线程数或最大线程数。
优势与注意事项
- 优势
- CAS 减少锁的使用,提升任务分配和状态更新的性能。
- 保证线程池状态的线程安全,避免死锁。
- 注意事项
- 高并发下 CAS 自旋可能增加 CPU 开销。
- 复杂状态转换可能需要结合锁(如 mainLock)。
总结
- AtomicInteger 的增减操作:通过 CAS 实现无锁计数,适合高并发计数器场景,需注意自旋开销。
- ConcurrentHashMap 的内部实现:结合 CAS 和分段锁,支持高效并发读写,广泛用于缓存和共享数据。
- 线程池的任务分配和状态更新:CAS 管理线程计数和状态变更,确保任务调度和状态切换的线程安全。
坚持下去,会见曙光~