每日学习Java之一万个为什么(JUC)
文章目录
- Git复习
- synchronized
- 介绍
- 基本概念
- 特点
- 使用模板
- 1. 同步方法
- 格式
- 特点
- 2. 同步代码块
- 格式
- 特点
- 常见面试题
- 1. `synchronized`的实现原理?
- 2. `synchronized`与`ReentrantLock`的区别?
- 3. `synchronized`的缺点?
- 4. 死锁的四个必要条件?
- 5. 如何避免死锁?
- 6. `synchronized`是重量级锁吗?
- 7. `synchronized`修饰静态方法时锁对象是什么?
- ReentrantLock
- 介绍
- 基本概念
- 实现原理
- 特点
- 使用模板
- 1. 基础用法(非公平锁)
- 2. 公平锁
- 3. 尝试获取锁(非阻塞)
- 4. 带超时的尝试获取锁
- 常用方法
- 常见面试题
- 1. `ReentrantLock`与`synchronized`的区别?
- 2. `ReentrantLock`的可重入性如何实现?
- 3. `ReentrantLock`的公平锁与非公平锁有何区别?
- 4. 如何避免`ReentrantLock`的锁泄漏?
- 5. `Condition`的作用是什么?如何使用?
- 6. `ReentrantLock`的锁降级是否可行?
- 7. `ReentrantLock`的`tryLock()`与`synchronized`的区别?
- 总结
- Condition实现指定唤醒线程 以及代替wait/notify在Synchronized中的地位
- Condition介绍
- 使用模板
- demo:指定唤醒线程(ABC轮流使用资源类)
- demo:代替wait/notify在Synchronized中的地位(包括synchronized使用)
- Condition
- 常用方法
- 核心方法
- 其他方法
- 常见面试题
- 1. **Condition 的作用是什么?与 `wait/notify` 有何区别?**
- 2. **为什么 `await()` 必须在持有锁时调用?**
- 3. **`signal()` 和 `signalAll()` 的区别?**
- 4. **`Condition` 如何实现精准唤醒?**
- 5. **`Condition` 的实现原理?**
- 6. **使用 `Condition` 时的注意事项?**
- 7. **`Condition` 的典型应用场景?**
- 8. **`Condition` 与 `synchronized` 的 `wait/notify` 的兼容性?**
- 9. **`await()` 可能抛出的异常?**
- 10. **如何实现带超时的条件等待?**
- 总结
- JUC 容器类 CopyOnWriteXXX / ConcurrentHashMap
- 常见面试题
- **1. JUC中常见的线程安全容器有哪些?**
- **2. ConcurrentHashMap与Hashtable的区别?**
- **3. 为什么JDK1.8后ConcurrentHashMap舍弃分段锁?**
- **4. CopyOnWriteArrayList的适用场景?**
- **5. BlockingQueue的实现类有哪些?分别适用于什么场景?**
- **6. 为什么JUC的ConcurrentHashMap要用synchronized而不是ReentrantLock?**
- **7. 如何确保一个集合不能被修改?**
- **8. 什么是fail-fast和fail-safe?JUC容器如何实现?**
- **9. 说一下ConcurrentHashMap的实现原理(JDK1.8)?**
- **10. BlockingQueue的take()和poll()有什么区别?**
- **11. 如何选择线程安全的集合类?**
- **12. 为什么Vector是遗留类?如何替代?**
- **总结**
- JUC 辅助类 CountDownLatch / CyclicBarrier / Semophore
- CountDownLatchDemo(倒计时)
- CyclicBarrierDemo(收集器)
- SemaphoreDemo (信号量)可以代替锁
- **CountDownLatch**
- **常用方法**
- **使用场景**
- **常见面试题**
- **CyclicBarrier**
- **常用方法**
- **使用场景**
- **常见面试题**
- **Semaphore**
- **常用方法**
- **使用场景**
- **常见面试题**
- **总结**
Git复习
以下是补充后的笔记内容,结构清晰,涵盖核心知识点和常见面试题:
synchronized
介绍
基本概念
- 作用:Java中的
synchronized
关键字用于实现线程同步,确保同一时间只有一个线程访问被其修饰的代码块或方法,避免线程安全问题(如竞态条件、脏读等)。 - 实现机制:基于Java内存模型(JMM)的监视器锁(Monitor),通过
对象头
中的Mark Word记录锁状态,支持偏向锁、轻量级锁、重量级锁的升级机制。 - 适用场景:简单同步需求,代码简洁,但性能可能受限于锁升级后的重量级锁。
特点
- 原子性:保证代码块或方法的原子操作。
- 可见性:线程间共享变量的修改对其他线程可见。
- 阻塞式同步:线程获取锁失败时会阻塞等待。
使用模板
1. 同步方法
格式
// 实例方法(锁对象为this)
public synchronized void method() { ... }// 静态方法(锁对象为类的Class对象)
public static synchronized void staticMethod() { ... }
特点
- 锁对象:
- 实例方法:当前对象实例(
this
)。 - 静态方法:类的
Class
对象(ClassName.class
)。
- 实例方法:当前对象实例(
2. 同步代码块
格式
// 对象锁(锁对象为obj)
synchronized(obj) {// 同步代码块
}// 示例:使用this作为锁
synchronized(this) { ... }// 示例:使用类锁(静态方法等效)
synchronized(ClassName.class) { ... }
特点
- 灵活性:可自定义锁对象,避免全类或全实例的锁竞争。
- 推荐实践:锁对象应为私有且不可变,避免被外部误用。
常见面试题
1. synchronized
的实现原理?
- Monitor对象:JVM通过
对象头
中的Mark Word记录锁状态。 - 锁状态升级:
- 偏向锁:默认为线程分配偏向锁,减少同步开销(JDK6引入)。
- 轻量级锁:通过CAS尝试获取锁,失败则升级为重量级锁。
- 重量级锁:阻塞线程,依赖操作系统互斥量(性能开销大)。
2. synchronized
与ReentrantLock
的区别?
特性 | synchronized | ReentrantLock |
---|---|---|
中断性 | 不支持 | 支持(tryLock() 可中断) |
超时 | 不支持 | 支持(tryLock(long time) ) |
公平性 | 非公平 | 可配置公平或非公平 |
功能 | 简单易用 | 更灵活(如条件变量Condition ) |
性能 | 重量级锁时性能较差 | 通常更高(但需合理使用) |
3. synchronized
的缺点?
- 不可中断:线程获取锁时无法中断。
- 死锁风险:若多个线程互相等待锁,可能引发死锁。
- 性能问题:重量级锁可能导致线程阻塞开销大。
- 粒度控制不足:同步方法无法灵活控制锁范围。
4. 死锁的四个必要条件?
- 互斥:资源不可共享,同一时间只能被一个线程占用。
- 请求与保持:线程持有已分配资源,仍请求新资源。
- 不剥夺:资源无法被强制剥夺,只能由持有者释放。
- 循环等待:存在线程循环等待链(如A等待B的资源,B等待A的资源)。
5. 如何避免死锁?
- 按顺序获取锁:所有线程按固定顺序申请锁。
- 超时机制:使用
tryLock()
并设置超时时间。 - 尽早释放锁:减少锁持有时间,避免在锁内执行耗时操作。
6. synchronized
是重量级锁吗?
- 早期JVM:是,依赖操作系统互斥量,开销大。
- JDK6+:引入锁升级机制,优先使用偏向锁和轻量级锁,性能显著提升。
7. synchronized
修饰静态方法时锁对象是什么?
- 锁对象为类的Class对象(即
ClassName.class
)。
以下是补充后的 ReentrantLock
笔记,结构清晰且覆盖核心知识点和常见面试题:
ReentrantLock
介绍
基本概念
- 作用:Java中用于实现线程同步的显式锁,需手动获取和释放,属于
java.util.concurrent.locks
包。 - 可重入性:支持同一线程多次获取同一锁(递归锁),避免死锁。
- 公平性:可选择公平模式(线程按排队顺序获取锁)或非公平模式(允许插队,性能更高)。
实现原理
- 底层机制:基于**AQS(AbstractQueuedSynchronizer)**实现,通过CAS操作竞争锁。
- 锁状态:通过内部计数器记录锁的持有次数,线程释放锁时需计数归零。
特点
- 灵活性:支持尝试获取锁(
tryLock()
)、超时获取锁、条件变量(Condition
)等高级功能。 - 需手动管理:必须显式调用
lock()
和unlock()
,否则可能导致死锁或资源泄漏。
使用模板
1. 基础用法(非公平锁)
ReentrantLock lock = new ReentrantLock();// 获取锁
lock.lock();
try {// 同步代码块
} finally {lock.unlock(); // 必须确保释放锁
}
2. 公平锁
ReentrantLock fairLock = new ReentrantLock(true); // true表示公平模式
3. 尝试获取锁(非阻塞)
// 尝试立即获取锁,失败返回false
if (lock.tryLock()) {try {// 同步代码块} finally {lock.unlock();}
}
4. 带超时的尝试获取锁
// 尝试在1秒内获取锁,超时返回false
if (lock.tryLock(1, TimeUnit.SECONDS)) {try {// 同步代码块} finally {lock.unlock();}
}
常用方法
方法 | 说明 |
---|---|
lock() | 阻塞等待获取锁,直到成功或被中断。 |
unlock() | 释放锁,需确保在finally块中调用。 |
tryLock() | 非阻塞尝试获取锁,成功返回true,失败返回false。 |
tryLock(long time) | 在指定时间内尝试获取锁,超时返回false。 |
isHeldByCurrentThread() | 判断当前线程是否持有该锁。 |
getHoldCount() | 返回当前线程持有的锁次数(可重入计数)。 |
newCondition() | 创建与该锁绑定的Condition 对象,用于线程间协作。 |
常见面试题
1. ReentrantLock
与synchronized
的区别?
特性 | ReentrantLock | synchronized |
---|---|---|
中断性 | 支持(lockInterruptibly() ) | 不支持 |
超时 | 支持(tryLock() ) | 不支持 |
公平性 | 可配置(公平/非公平) | 默认非公平 |
功能 | 支持Condition ,更灵活 | 内置锁,无需手动释放 |
性能 | 通常更高(无锁升级机制) | JDK6+优化后性能接近 |
语法 | 显式调用lock() /unlock() | 隐式管理 |
2. ReentrantLock
的可重入性如何实现?
- 通过内部计数器记录锁的持有次数:
- 线程首次获取锁时计数器设为1。
- 再次获取锁时计数器递增。
- 每次
unlock()
计数器递减,归零时释放锁。
3. ReentrantLock
的公平锁与非公平锁有何区别?
- 公平锁:线程按排队顺序获取锁,避免“饥饿”但性能稍差。
- 非公平锁:允许新线程插队尝试获取锁(CAS竞争),性能更高但可能引发饥饿。
4. 如何避免ReentrantLock
的锁泄漏?
- 必须确保
unlock()
在finally块中调用,即使发生异常也要释放锁:lock.lock(); try {// 业务逻辑 } finally {lock.unlock(); }
5. Condition
的作用是什么?如何使用?
- 作用:提供线程等待/唤醒机制,替代
synchronized
的wait()
/notify()
。 - 使用示例:
Condition condition = lock.newCondition();// 等待条件 condition.await();// 唤醒一个等待线程 condition.signal();// 唤醒所有等待线程 condition.signalAll();
6. ReentrantLock
的锁降级是否可行?
- 不可直接实现:需通过二级锁(如
ReentrantReadWriteLock
)实现读写锁的降级。
7. ReentrantLock
的tryLock()
与synchronized
的区别?
tryLock()
是非阻塞的,失败直接返回false,而synchronized
会阻塞等待。tryLock()
需配合unlock()
手动释放锁。
总结
- 适用场景:需要高级功能(如超时、条件变量)或对性能要求高时。
- 注意事项:必须确保锁的释放,避免泄漏;合理选择公平性模式。
- 替代方案:
synchronized
适合简单场景,ReentrantLock
适合复杂需求。
Condition实现指定唤醒线程 以及代替wait/notify在Synchronized中的地位
Condition介绍
使用模板
demo:指定唤醒线程(ABC轮流使用资源类)
package com.qxy.practice.concurrent;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @author : 戚欣扬* @Description : // v4 对Condtion的灵活运用,指定唤醒线程 A 5->B 10->C 15 loop 10*/
public class ThreadOrder {private int flag = 1;private Lock lock = new ReentrantLock();private Condition key1 = lock.newCondition();private Condition key2 = lock.newCondition();private Condition key3 = lock.newCondition();/*** print 1-5*/public void print5(){lock.lock();try {while(flag!=1){key1.await();}for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName()+":"+(i+1));}flag = 2;key2.signal();} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}/*** print 1-10*/public void print10(){lock.lock();try {while(flag!=2){key1.await();}for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+":"+(i+1));}flag = 3;key2.signal();} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}/*** print 1-15*/public void print15(){lock.lock();try {while(flag!=3){key1.await();}for (int i = 0; i < 15; i++) {System.out.println(Thread.currentThread().getName()+":"+(i+1));}flag = 1;key2.signal();} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}/*** main*/public static void main(String[] args) {ThreadOrder threadOrder = new ThreadOrder();for (int i = 0; i < 10; i++) {new Thread(()->{threadOrder.print5();},"A").start();new Thread(()->{threadOrder.print10();},"B").start();new Thread(()->{threadOrder.print15();},"C").start();}}}
demo:代替wait/notify在Synchronized中的地位(包括synchronized使用)
package com.qxy.practice.concurrent;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @author : 戚欣扬* @code :*/
public class ThreadTransmit {private int num = 0 ;private Lock lock = new ReentrantLock();Condition condition1 = lock.newCondition();Condition condition2 = lock.newCondition();Condition condition3 = lock.newCondition();
// V1 使用公平锁 / 睡眠实现 1+ 1-
/* private Lock lock = new ReentrantLock();public void increase(){lock.lock();try{num++;System.out.println(Thread.currentThread().getName()+"num ="+num);}finally {lock.unlock();}}public void decrease(){lock.lock();try{num--;System.out.println(Thread.currentThread().getName()+"num ="+num);}finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {ThreadTransmit threadTransmit = new ThreadTransmit();for (int i = 0; i < 10; i++) {new Thread(()->{threadTransmit.increase();},"A").start();new Thread(()->{threadTransmit.decrease();},"B").start();}}*/
// V2 使用synchronized+wait+notify 实现 虚假唤醒问题 死锁问题
/* public synchronized void increase(){//判断while(num!=0){try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}//执行System.out.println(Thread.currentThread().getName()+":num = " + (++num));//通知this.notifyAll();}public synchronized void decrease(){//判断while(num!=1){try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}//执行System.out.println(Thread.currentThread().getName()+":num = " + (--num));//通知this.notifyAll();}public static void main(String[] args) {ThreadTransmit threadTransmit = new ThreadTransmit();new Thread(()->{for (int i = 0; i < 10; i++) {threadTransmit.increase();}},"A").start();new Thread(()->{for (int i = 0; i < 10; i++) {threadTransmit.decrease();}},"B").start();new Thread(()->{for (int i = 0; i < 10; i++) {threadTransmit.increase();}},"C").start();new Thread(()->{for (int i = 0; i < 10; i++) {threadTransmit.decrease();}},"D").start();}*/
// v3 lock.lock() +condition.await() + condition.signalAll()
public void increase(){lock.lock();try {//判断while(num!=0){try {condition1.await();} catch (InterruptedException e) {throw new RuntimeException(e);}}//执行System.out.println(Thread.currentThread().getName()+":num = " + (++num));//通知condition1.signalAll();} finally {lock.unlock();}
}public void decrease(){lock.lock();try {//判断while(num!=1){try {condition1.await();} catch (InterruptedException e) {throw new RuntimeException(e);}}//执行System.out.println(Thread.currentThread().getName()+":num = " + (--num));//通知condition1.signalAll();} finally {lock.unlock();}}public static void main(String[] args) {ThreadTransmit threadTransmit = new ThreadTransmit();new Thread(()->{for (int i = 0; i < 10; i++) {threadTransmit.increase();}},"A").start();new Thread(()->{for (int i = 0; i < 10; i++) {threadTransmit.decrease();}},"B").start();new Thread(()->{for (int i = 0; i < 10; i++) {threadTransmit.increase();}},"C").start();new Thread(()->{for (int i = 0; i < 10; i++) {threadTransmit.decrease();}},"D").start();}
// v4 A 5->B 10->C 15 loop 10// concurrentModifyException
}
以下是对 Condition 的补充笔记,涵盖 常用方法 和 常见面试题,结合知识库内容整理:
Condition
常用方法
核心方法
方法 | 说明 | 注意事项 |
---|---|---|
await() | 使当前线程进入等待状态,直到被通知或中断。 | 必须在持有锁时调用,否则抛出 IllegalMonitorStateException 。 |
signal() | 唤醒一个等待在该 Condition 上的线程。 | 必须在持有锁时调用,唤醒的线程会进入同步队列竞争锁。 |
signalAll() | 唤醒所有等待在该 Condition 上的线程。 | 适用于需要通知所有等待线程的场景(如资源充足时唤醒所有消费者)。 |
await(long time, TimeUnit unit) | 在指定时间内等待,超时返回 false 。 | 超时后线程会自动释放锁并返回。 |
awaitUninterruptibly() | 忽略中断,等待直到被通知。 | 即使线程被中断也不会抛出异常。 |
awaitNanos(long nanosTimeout) | 基于纳秒时间等待,返回剩余时间(若超时返回负数)。 | 精度高但需注意时间单位转换。 |
其他方法
方法 | 说明 |
---|---|
awaitUntil(Date deadline) | 在指定截止时间前等待,超时返回 false 。 |
isAlive() | 检查线程是否存活(实际由线程自身判断,Condition不直接提供此方法)。 |
常见面试题
1. Condition 的作用是什么?与 wait/notify
有何区别?
- 作用:
Condition 是 Java 并发包中用于线程间协作的接口,提供更灵活的条件等待和通知机制,需与Lock
配合使用。 - 与
wait/notify
的区别:- 多条件支持:一个
Lock
可创建多个Condition
对象,每个条件对应独立的等待队列(如生产者-消费者中的“队列满”和“队列空”)。 - 精准唤醒:
signal()
可精准唤醒特定条件的线程,而notify()
可能随机唤醒线程;signalAll()
仅唤醒当前 Condition 的队列,比notifyAll()
更高效。 - 显式绑定:Condition 需显式绑定到
Lock
,而wait/notify
依赖隐式Object
的监视器锁。
- 多条件支持:一个
2. 为什么 await()
必须在持有锁时调用?
- 原因:
- 调用
await()
会释放当前持有的锁,允许其他线程获取锁。 - 若未持有锁就调用,会抛出
IllegalMonitorStateException
。
- 调用
- 流程:
lock.lock(); try {while (conditionNotMet) {condition.await(); // 释放锁并等待}// 执行业务逻辑condition.signal(); // 唤醒其他线程 } finally {lock.unlock(); // 重新获取锁后释放 }
3. signal()
和 signalAll()
的区别?
signal()
:
随机唤醒等待队列中的一个线程,使其竞争锁。适用于“只需一个线程响应”的场景(如消费者消费一个商品)。signalAll()
:
唤醒所有等待线程,它们会竞争锁。适用于“所有线程都需要处理变化”的场景(如资源充足时唤醒所有消费者)。- 注意:
唤醒后线程需重新竞争锁,最终只有一个线程能执行临界区代码。
4. Condition
如何实现精准唤醒?
- 多条件队列:
一个Lock
可创建多个Condition
,每个 Condition 对应独立的等待队列。例如:Condition notFull = lock.newCondition(); // 生产者等待条件 Condition notEmpty = lock.newCondition(); // 消费者等待条件
- 选择性通知:
生产者在添加元素后调用notEmpty.signal()
,仅唤醒消费者队列中的线程;消费者在移除元素后调用notFull.signal()
,唤醒生产者队列中的线程。
5. Condition
的实现原理?
- 底层机制:
基于 AQS(AbstractQueuedSynchronizer) 实现,每个 Condition 对象维护一个 等待队列。- await():
- 释放锁,当前线程加入 Condition 的等待队列。
- 线程被阻塞,直到被信号唤醒或超时。
- signal():
- 将等待队列中的线程移至 AQS 的同步队列,使其有机会重新竞争锁。
- 线程获取锁后继续执行。
- await():
6. 使用 Condition
时的注意事项?
- 必须在锁保护下调用:
await()
、signal()
必须在lock
保护的代码块中调用。 - 避免虚假唤醒:
使用 循环检查条件,而非单一判断(如生产者-消费者中需检查队列是否满/空)。 - 避免死锁:
确保unlock()
在finally
块中调用,避免因异常导致锁未释放。 - 谨慎使用
awaitUninterruptibly()
:
可能忽略中断信号,需根据业务场景权衡。
7. Condition
的典型应用场景?
- 生产者-消费者模式:
// 生产者 lock.lock(); try {while (queue.isFull()) {notFull.await(); // 队列满时等待}queue.put(item);notEmpty.signal(); // 唤醒消费者 } finally {lock.unlock(); }
- 读写锁:通过多个 Condition 实现读写线程的协调。
- 线程池任务调度:根据任务队列状态通知空闲线程。
8. Condition
与 synchronized
的 wait/notify
的兼容性?
- 不可混用:
Condition
必须与Lock
配合使用,不能与synchronized
的wait/notify
混合。- 若混用会导致锁状态混乱,可能抛出
IllegalMonitorStateException
。
9. await()
可能抛出的异常?
- InterruptedException:
当线程在等待时被中断,会抛出此异常,需在catch
块中处理。 - IllegalMonitorStateException:
未持有锁时调用await()
或signal()
会抛出此异常。
10. 如何实现带超时的条件等待?
- 使用
await(long time, TimeUnit unit)
或awaitNanos()
:if (condition.await(1, TimeUnit.SECONDS)) {// 在1秒内被唤醒,继续执行 } else {// 超时处理 }
总结
- 核心价值:Condition 提供了比
wait/notify
更灵活的线程协作能力,支持多条件管理和精准唤醒。 - 使用要点:必须与
Lock
配合,确保锁的正确释放和获取,避免死锁和资源泄漏。 - 替代方案:简单场景可使用
synchronized
,复杂场景推荐ReentrantLock + Condition
。
JUC 容器类 CopyOnWriteXXX / ConcurrentHashMap
package com.qxy.practice.concurrent;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;/*** @author : 戚欣扬* @Description : 线程同步队列的两种使用*/
public class SyncListDemo {/*** 如果我们使用ArrayList造100个线程去读写同一个list,会报ConcurrentModifyException :* if (to > es.length) {* throw new ConcurrentModificationException();* }* 1.使用工具类转换为线程安全集合 Collections. 2.使用 CopyOnWirte 读写分离,写时复制*/public static void main(String[] args) {// List<String> list = Collections.synchronizedList(new ArrayList<>());//new ArrayList<>();List<String> list = new CopyOnWriteArrayList<>();// HashSet 同理 HashMap 采用 ConcurrentHashMapfor (int i = 0; i < 100; i++) {new Thread(()->{list.add(UUID.randomUUID().toString().substring(0,6));System.out.println(list);},String.valueOf(i)).start();}}
}
常见面试题
1. JUC中常见的线程安全容器有哪些?
答案:
- ConcurrentHashMap:线程安全的哈希表,替代Hashtable,支持部分并发操作。
- CopyOnWriteArrayList:线程安全的动态数组,写时复制(Read-Copy-Update)实现。
- CopyOnWriteArraySet:基于CopyOnWriteArrayList的线程安全Set实现。
- ConcurrentSkipListMap/Set:基于跳表(SkipList)的有序线程安全集合。
- BlockingQueue:阻塞队列(如ArrayBlockingQueue、LinkedBlockingQueue等),支持生产者-消费者模式。
- ConcurrentLinkedQueue:非阻塞的线程安全队列,基于链表实现。
2. ConcurrentHashMap与Hashtable的区别?
答案:
特性 | ConcurrentHashMap | Hashtable |
---|---|---|
线程安全 | 部分线程安全(分段锁/JDK1.8后CAS实现) | 全局锁(所有方法同步) |
性能 | 高并发下性能更好(分段锁/JDK1.8后无锁化) | 低效(单线程操作) |
null值支持 | 允许null键和值(JDK8+) | 不允许null键或值 |
迭代器 | 失败为安全(fail-safe) | 失败快速(fail-fast) |
实现原理 | 分段锁(JDK1.7)或CAS+锁(JDK1.8+) | 全局synchronized 锁 |
3. 为什么JDK1.8后ConcurrentHashMap舍弃分段锁?
答案:
- 分段锁(Segment)的问题:
多个Segment锁仍存在竞争,且扩容时需锁所有Segment,性能受限。 - JDK1.8的改进:
- 基于CAS和锁:每个桶(Bucket)使用CAS操作,仅在哈希冲突时加锁单个桶。
- 无锁化设计:大部分读操作无需锁,写操作通过CAS和自旋实现。
- 数组+链表/红黑树:与HashMap结构一致,支持快速扩容和冲突处理。
- 优势:
显著提升写操作的并发性能,同时保持读操作的高效性。
4. CopyOnWriteArrayList的适用场景?
答案:
- 读多写少:写操作会复制整个数组,但读操作无需锁,适合读频繁场景。
- 批量操作:如日志记录、配置列表等,写操作不频繁。
- 避免迭代中断:迭代器是快照(fail-safe),不会抛出
ConcurrentModificationException
。
缺点:
- 内存开销大(写操作复制数组)。
- 写操作性能低,不适合频繁写入。
5. BlockingQueue的实现类有哪些?分别适用于什么场景?
答案:
实现类 | 特点 | 适用场景 |
---|---|---|
ArrayBlockingQueue | 基于数组的有界队列,FIFO。 | 固定容量的场景,需控制生产者速度。 |
LinkedBlockingQueue | 基于链表的可选有界队列,默认无界。 | 无界队列或需要缓存的场景。 |
SynchronousQueue | 不存储元素,每个put 必须等待一个take 。 | 线程间直接交换数据(如线程池)。 |
PriorityBlockingQueue | 基于优先级堆的无界队列,按优先级排序。 | 需按优先级处理任务的场景。 |
DelayQueue | 基于时间的延迟队列,元素只有在延迟时间到达后才能被取出。 | 定时任务或延迟任务队列。 |
LinkedTransferQueue | 支持无界队列和扩展操作(如转移、批量操作)。 | 需高效生产和消费的高并发场景。 |
6. 为什么JUC的ConcurrentHashMap要用synchronized而不是ReentrantLock?
答案:
- 兼容性和性能:
synchronized
在JVM层面优化更好,JDK1.6后引入偏向锁、轻量级锁等机制,性能接近ReentrantLock
。ConcurrentHashMap
的锁粒度较小(如分段锁或单桶锁),同步块较短,synchronized
的语法简洁性更优。
- 代码简洁性:
synchronized
的语法糖(自动释放锁)减少了代码复杂度,避免ReentrantLock
需要显式unlock()
的风险。
7. 如何确保一个集合不能被修改?
答案:
- 使用不可变集合:
Collections.unmodifiableCollection()
包装集合,返回只读视图。 - 使用CopyOnWrite容器:
如CopyOnWriteArrayList
,写操作会抛出UnsupportedOperationException
。 - 自定义实现:
继承集合类并重写修改方法,抛出异常或忽略操作。
8. 什么是fail-fast和fail-safe?JUC容器如何实现?
答案:
- fail-fast:
- 机制:迭代过程中检测到结构修改(如并发修改),抛出
ConcurrentModificationException
。 - 实现:通过
modCount
计数器,迭代器维护expectedModCount
,若不一致则抛异常。 - 容器:
ArrayList
、HashMap
等非线程安全集合。
- 机制:迭代过程中检测到结构修改(如并发修改),抛出
- fail-safe:
- 机制:迭代器基于快照(快照时的数据),忽略后续修改,不会抛异常。
- 实现:基于写时复制(如
CopyOnWriteArrayList
)或版本控制(如ConcurrentHashMap
)。 - 容器:
CopyOnWriteArrayList
、ConcurrentHashMap
等线程安全集合。
9. 说一下ConcurrentHashMap的实现原理(JDK1.8)?
答案:
- 结构:
- 基于数组+链表/红黑树结构,与HashMap一致。
- 数组元素为
Node
对象,每个节点包含键值对、哈希值、指针等。
- 并发控制:
- CAS操作:大部分操作通过CAS(
compareAndSwap
)无锁化完成。 - 锁粒度:仅在哈希冲突时锁单个桶(
synchronized
)。
- CAS操作:大部分操作通过CAS(
- 扩容机制:
- 扩容时分段进行,新旧表交替迁移数据,避免全局锁。
- 性能优化:
- 链表长度超过8转为红黑树,降低查找时间。
- 读操作无需锁,写操作通过CAS和局部锁实现。
10. BlockingQueue的take()和poll()有什么区别?
答案:
方法 | 行为 |
---|---|
take() | 阻塞等待,直到有元素可用或被中断。 |
poll() | 非阻塞,若队列为空立即返回null (或指定超时时间后返回)。 |
11. 如何选择线程安全的集合类?
答案:
- 读多写少:
CopyOnWriteArrayList
、ConcurrentHashMap
。 - 频繁写操作:
ConcurrentHashMap
(哈希表)或ConcurrentSkipListMap
(有序)。 - 队列需求:根据是否阻塞选择
BlockingQueue
或ConcurrentLinkedQueue
。 - 迭代安全:优先使用fail-safe容器(如
CopyOnWriteArrayList
)。
12. 为什么Vector是遗留类?如何替代?
答案:
- Vector的问题:
- 全局
synchronized
锁,性能低。 - 扩容为双倍容量,内存浪费。
- 全局
- 替代方案:
- 线程安全:
CopyOnWriteArrayList
(读多写少)或ConcurrentLinkedQueue
。 - 非线程安全:
ArrayList
(手动加锁)。
- 线程安全:
总结
- 核心思想:JUC容器通过减少锁粒度、无锁化操作(CAS)、写时复制等技术,在保证线程安全的同时提升性能。
- 设计模式:
- 分段锁(旧版ConcurrentHashMap)。
- 快照迭代(CopyOnWrite)。
- 阻塞队列(生产者-消费者模式)。
- 高频考点:ConcurrentHashMap的实现、CopyOnWrite的适用场景、BlockingQueue的类型选择。
JUC 辅助类 CountDownLatch / CyclicBarrier / Semophore
CountDownLatchDemo(倒计时)
package com.qxy.practice.concurrent;import java.util.concurrent.CountDownLatch;/*** @author : 戚欣扬* @Description :*/
public class CountDownLatchDemo {public static void main(String[] args) {CountDownLatch clock = new CountDownLatch(3);for (int i = 3; i >0; i--) {int finalI = i;new Thread(()->{System.out.println("世界即将崩坏,倒计时 :" + finalI);//事实上,这个println顺序应该是随机的clock.countDown();},String.valueOf(i)).start();}try {clock.await();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("天国降临!!!");}
}
CyclicBarrierDemo(收集器)
package com.qxy.practice.concurrent;import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;/*** @author : 戚欣扬* @Description :*/
public class CyclicBarrierDemo {public static void main(String[] args) {CyclicBarrier collect = new CyclicBarrier(7,()->{System.out.println("召唤神龙!超级赛亚人之神!!!");});for (int i = 1; i <8; i++) {int finalI = i;new Thread(()->{System.out.println("已经收集龙珠 :" + finalI);try {collect.await();} catch (InterruptedException e) {throw new RuntimeException(e);} catch (BrokenBarrierException e) {throw new RuntimeException(e);}},String.valueOf(i)).start();}}
}
SemaphoreDemo (信号量)可以代替锁
package com.qxy.practice.concurrent;import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;/*** @author : 戚欣扬* @Description :*/
public class SemaphoreDemo {Semaphore semaphore = new Semaphore(1);public void stopCar(){try {semaphore.acquire();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+" 抢到了车位 " );try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName()+" 离开了商场" );semaphore.release();}public static void main(String[] args) {SemaphoreDemo semaphoreDemo = new SemaphoreDemo();for (int i = 0; i < 5; i++) {new Thread(()->{semaphoreDemo.stopCar();},String.valueOf(i)).start();}}
}
以下是关于 JUC辅助类 CountDownLatch、CyclicBarrier、Semaphore 的常用方法、使用场景及常见面试题的整理:
CountDownLatch
常用方法
方法 | 说明 |
---|---|
countDown() | 将计数器减 1,当计数器为 0 时,所有等待的线程被唤醒。 |
await() | 阻塞当前线程,直到计数器为 0。 |
await(long timeout, TimeUnit unit) | 在指定时间内等待计数器为 0,超时返回 false 。 |
getCount() | 返回当前计数器的值。 |
使用场景
-
等待多个线程完成任务:
- 主线程等待所有子线程执行完毕后再继续(如启动服务时等待多个组件加载完成)。
- 示例:多个线程并行下载文件,主线程等待所有下载完成后合并文件。
-
并行任务协调:
- 多个线程需要同时开始执行任务(如赛跑中的“发令枪”)。
- 示例:初始化一个
CountDownLatch(1)
,多个线程在await()
处等待,主线程调用countDown()
后所有线程同时执行。
-
资源加载:
- 主线程依赖多个资源加载完成才能继续执行(如数据库连接、配置加载)。
常见面试题
-
CountDownLatch 的计数器如何变化?是否可重置?
- 计数器只能减少(通过
countDown()
),初始值由构造函数指定。 - 不可重置,一旦计数器为 0,后续调用
await()
会立即返回。
- 计数器只能减少(通过
-
CountDownLatch 和 CyclicBarrier 的区别?
特性 CountDownLatch CyclicBarrier 计数器 单次使用,不可重置 可循环使用 等待触发条件 计数器为 0 线程数达到阈值 应用场景 主线程等待多个线程完成 多线程需要多次同步到达同一屏障 -
CountDownLatch 如何实现并行任务的“同时开始”?
- 初始化
CountDownLatch(1)
,所有线程在await()
处等待。 - 主线程调用
countDown()
后,所有线程同时解除阻塞,开始执行。
- 初始化
-
CountDownLatch 的
await()
会被中断吗?- 会抛出
InterruptedException
,需在catch
块中处理。
- 会抛出
CyclicBarrier
常用方法
方法 | 说明 |
---|---|
await() | 阻塞当前线程,直到到达屏障的线程数等于阈值(parties )。 |
await(long timeout, TimeUnit unit) | 在指定时间内等待,超时返回 BrokenBarrierException 。 |
getNumberWaiting() | 返回正在等待的线程数。 |
isBroken() | 判断屏障是否处于破损状态(有线程中断或异常)。 |
使用场景
-
多线程协作同步:
- 多个线程需要在某个点汇合后再继续执行(如分布式计算的阶段同步)。
- 示例:多个线程分别处理数据后,到达屏障汇总结果。
-
循环使用场景:
- 需要多次重置的同步点(如模拟比赛的多轮竞赛)。
-
分阶段任务处理:
- 任务需要分阶段执行,每个阶段需所有线程完成后再进入下一阶段。
常见面试题
-
CyclicBarrier 的
await()
如何触发?- 当调用
await()
的线程数等于构造时指定的parties
值时触发,所有线程被释放。
- 当调用
-
CyclicBarrier 的屏障如何复用?
- 触发后自动重置,可多次使用。除非屏障被破坏(
broken
标志为true
),否则可循环使用。
- 触发后自动重置,可多次使用。除非屏障被破坏(
-
CyclicBarrier 和 CountDownLatch 的核心区别?
- CountDownLatch:单向计数,不可重置,主线程等待其他线程完成。
- CyclicBarrier:双向同步,可循环使用,所有线程需到达屏障后共同继续。
-
如何通过 CyclicBarrier 执行屏障到达后的操作?
- 使用带
Runnable
的构造函数CyclicBarrier(int parties, Runnable barrierAction)
,当线程数达到阈值时,会执行barrierAction
。
- 使用带
Semaphore
常用方法
方法 | 说明 |
---|---|
acquire() | 获取一个许可证,若无可用许可证则阻塞。 |
acquire(int permits) | 获取指定数量的许可证。 |
release() | 释放一个许可证。 |
release(int permits) | 释放指定数量的许可证。 |
tryAcquire() | 尝试获取一个许可证,失败立即返回 false 。 |
tryAcquire(long timeout, TimeUnit unit) | 在指定时间内尝试获取许可证。 |
availablePermits() | 返回当前可用的许可证数量。 |
使用场景
-
资源访问控制:
- 限制并发访问共享资源(如数据库连接池、线程池)。
- 示例:控制同时访问某文件的线程数不超过 5 个。
-
流量控制:
- 限流(如 API 请求限流,每秒最多 100 个请求)。
-
多线程协作:
- 协调线程的执行顺序(如生产者-消费者模式中控制生产者速率)。
常见面试题
-
Semaphore 的公平模式与非公平模式的区别?
- 公平模式:按线程等待顺序分配许可证。
- 非公平模式:允许插队,性能更高但可能引发饥饿。
-
Semaphore 如何实现资源池?
- 初始化
Semaphore(poolSize)
,每个线程需acquire()
获取资源,使用后release()
归还。
- 初始化
-
Semaphore 和 ReentrantLock 的区别?
- Semaphore:控制并发线程数量,支持多许可证。
- ReentrantLock:互斥锁,控制单个资源的访问。
-
如何避免 Semaphore 的死锁?
- 确保线程在获取许可证后最终释放(如在
finally
块中调用release()
)。
- 确保线程在获取许可证后最终释放(如在
-
acquire()
和tryAcquire()
的区别?acquire()
:阻塞等待直到获取许可证。tryAcquire()
:非阻塞,立即返回是否成功。
总结
类 | 核心功能 | 典型场景 |
---|---|---|
CountDownLatch | 等待多个线程完成任务,单向计数。 | 主线程等待所有子线程完成。 |
CyclicBarrier | 多线程在屏障处汇合,可循环使用。 | 多阶段任务同步、分布式计算。 |
Semaphore | 控制并发线程数量,支持资源访问限流。 | 资源池管理、流量控制、并发限制。 |