20250121面试鸭特训营第29天
更多特训营笔记详见个人主页【面试鸭特训营】专栏
250121
1. 你了解 Java 线程池的原理吗?
- 线程池是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。
线程池的 7 个参数
- corePoolSize
- 核心线程数,线程池中始终存活的线程数,即使它们是空闲的 。
- maximumPoolSize
- 最大线程数,线程池中允许的最大线程数。
- keepAliveTime
- 线程空闲时间,当线程数大于核心线程数时,空闲线程在等待新任务到达的最大时间 ,超过这个时间的非核心线程会被销毁。
- unit
- keepAliveTime的时间单位,可以是天、小时、分钟、秒等 。
- workQueue
- 一个阻塞队列,用来存储线程池等待执行的任务 。
- 工作队列的类型有
- SynchronousQueue:不存储任务,直接将任务提交给线程。
- LinkedBlockingQueue:链表结构的阻塞队列,大小无限。
- ArrayBlockingQueue:数据结构的有界阻塞队列。
- PriorityBlockingnQueue:带优先级的无界阻塞队列。
- threadFactory
- 线程工厂,主要用来创建线程,默认为正常优先级、非守护线程 。
- handler
- 拒绝策略,当任务过多(处理不过来)时提供的策略 。
线程池的工作原理
- 首先使用核心线程来执行任务 。
- 如果核心线程都忙,且任务队列未满,任务会被放入任务队列中 。
- 如果核心线程都忙,且任务队列已满,且线程数小于最大线程数,线程池会创建非核心线程来处理任务 。
- 如果核心线程都忙,且任务队列已满,如果线程数已经达到最大线程数,线程池会采取拒绝策略来处理新提交的任务 。
- 如果线程空闲时间超过空闲存活时间,并且当前线程数大于核心线程数,则会销毁线程,直到线程数等于核心线程数。(
allowCoreThreadTimeOut
默认为 false,设置为 true 可以回收核心线程)
4种拒绝策略
- AbortPolicy:丢弃任务,并抛出
RejectedExecutionException
异常 - CallerRunsPolicy:由调用线程(提交任务的线程)来处理该任务
- DiscardPolicy:直接丢弃无法处理的任务,不进行任何特殊处理
- DiscardOldestPolicy:丢弃队列中等待最久的任务,然后尝试再次提交当前任务
为什么先用阻塞队列而不是直接增加线程
- 因为创建线程需要占用一定系统资源(栈空间、线程调度开销等),导致性能下降。
- 使用阻塞队列可能将任务暂存,避免直接增加线程数带来的资源消耗。
- 如果阻塞队列都满了,说明系统负荷太大了,这时候再增加线程到最大线程数去消化任务。
- 举例
- 老板有 10 个员工(10个核心线程数),现在 10 个人手里都有活在干(10 个线程都有任务正在执行)。
- 这时候如果又来了 5 个活(5个任务),老板肯定不会立马再招 5 个人(新建线程)来干活,而是积攒着这些活(把任务放入阻塞队列)。
- 但是如果老板发现活积累的实在太多了(阻塞队列满了),才会继续招人干活(直接招满,相当于达到最大线程数)。
2. 你使用过哪些 Java 并发工具类?
ConcurrentHashMap
- 线程安全的哈希表,提供了高效的并发操作。
- 使用分段锁机制,多个线程可以同时访问不同的段,提高了并发性能。
- 提供了类似于
Map
的基本操作,如get
、put
、remove
,同时还支持并发修改。 - 适用于读多写少的场景,可以提供高并发的读写性能。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.putIfAbsent("key2", 2); // 如果key2不存在,才会放入
map.putIfAbsent("key2", 3); // 由于key2已存在,不会放入
int value1 = map.get("key1");
System.out.println("value1 = " + value1); // 打印value1 = 1
int value2 = map.get("key2");
System.out.println("value2 = " + value2); // 打印value2 = 2
AtomicInteger
- 是一个线程安全的整数类,用于对 int 类型进行原子性操作(如加减、比较、交换),保证了操作的线程安全性。
- 使用 CAS 机制,避免了加锁。
- 适用于需要频繁修改整数的无锁场景,例如计数器、标志位等。
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 自增1
count.decrementAndGet(); // 自减1
count.addAndGet(5); // 加5
int num = count.get(); // 获取当前值
System.out.println("num = " + num); // 打印num = 5
Semaphore
- 是一种信号量,在资源有限的情况下, 控制同一时刻访问共享资源的线程数量。
-
- 信号量的计数器表示可用的资源数量,线程获取资源时计数器减一,释放资源时计数器加一。
- 如果计数器为零,线程会被阻塞直到有资源可用。
- 通过计数器来控制访问资源的线程数量,适用于限制并发访问资源的场景。
Semaphore semaphore = new Semaphore(3); // 允许最多3个线程并发访问
try {
semaphore.acquire(); // 获取许可
// 执行任务
} finally {
semaphore.release(); // 释放许可
}
CyclicBarrier
- 是一个同步工具类,它使得一组线程可以相互等待,直到所有线程都到达某个时间点(例如等待其他线程完成某个阶段),然后一起继续执行。
- 适用于需要所有线程在某个时间点都完成后再继续的场景。
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障点")
});
Rannable task = () -> {
try {
// 执行任务
barrier.await(); // 等待其他线程
} catch (Exception e){
e.printStackTrace();
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
CountDownLatch
- 是一个同步工具类,用于使一个或多个线程等待,直到其他线程全都完成操作。
- 适用于主线程需要等待多个子线程完成任务的场景。
CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> {
try {
// 执行任务
} finally {
latch.countDown(); // 任务完成,计数器减一
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
latch.await(); // 等待计数器减到0,即等待所有任务都完成
System.out.println("所有任务都完成了");
BlockingQueue
- 是一个支持线程安全的队列,它可以在队列为空时阻塞取元素操作,在队列满时阻塞插入元素操作,适用于生产者-消费者模式。
- 生产者线程将元素放入队列,消费者线程从队列中取元素,队列为空时消费者线程阻塞,队列为满时生产者线程阻塞。
BlockingQueue<String> queue = new ArrayBlockingQueue<>();
// 生产者线程
Runnable producer = () -> {
try {
queue.put("item"); // 放入元素,如果队列满,则阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 消费者线程
Runnable consumer = () -> {
try {
String item = queue.take(); // 取出元素,如果队列为空,则阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(producer).start();
new Thread(consumer).start();
3. 什么是 Java 的 CAS(Compare-And-Swap)操作?
基本概念
- CAS 是一种硬件级别的原子操作,主要有三个参数,待更新的内存地址、期望值、新值。
- CAS 比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。
- 比较(Compare):CAS 会检查内存中的某个值是否与预期值相等。
- 交换(Swap):如果相等,则将内存中的值更新为新值。
- 失败重试:如果不相等,说明有其他线程已经修改了该值。CAS 操作失败,一般会重试直到成功。
举例说明
int a = 0;
// a++操作在多线程环境下是不安全的
a++; // 分为三步,1.取a的值 2.a+1 3.把新值写回a
System.out.println("a == " + a);
- 这种情况加锁是可以解决的,但是加锁过于消耗资源。
- CAS 操作的解决方案
-
- CAS 的三个参数:内存地址(a的内存地址)、期望值(1)、新值(2)。
- 线程 A 通过 a 的内存地址找到 a ,比较 a 的当前值 1 与期望值 1 是否相同。
- 若相同,则线程 A 将 a 的值更新为新值 2 。
- 若不相同,说明 a 已经被其他线程修改,需要重新尝试修改。
CAS 的优缺点
- 优点
-
- 无锁并发:CAS 操作不使用锁,因此不会导致线程阻塞,提高了系统的并发性和性能。
- 原子性:CAS 操作是原子的,保证了线程安全。
- 缺点
-
- ABA 问题:CAS 操作中,如果一个变量值从 A 变成 B 又变回 A,CAS 无法检测到这种情况,可能导致错误。
- 自旋开销:CAS 操作通常通过自旋实现,可能导致 CPU 资源浪费,尤其是在高并发情况下。
- 单变量限制:CAS 操作仅适用于单个变量的更新,不适用于涉及多个变量的复杂操作。