JUC 包详解
目录一、什么是 JUC二、并发集合线程安全的容器2.1 ConcurrentHashMap2.2 CopyOnWriteArrayList2.3 BlockingQueue阻塞队列三、锁3.1 Lock 接口 vs synchronized3.2 ReentrantLock可重入锁3.3 ReentrantReadWriteLock读写锁3.4 StampedLockJava 8 引入四、同步工具类4.1 CountDownLatch倒计时器4.2 CyclicBarrier循环栅栏4.3 Semaphore信号量4.4 Exchanger交换器五、线程池5.1 为什么要用线程池5.2 ThreadPoolExecutor 七大参数5.3 线程池的五种状态5.4 Executors 快捷创建线程池不推荐5.5 线程池大小如何设置六、常见面试题与回答话术6.1 基础题6.2 线程池专项6.3 同步工具专项6.4 进阶题6.5 代码题七、一句话快速记忆一、什么是 JUCJUCjava.util.concurrent包的简称Java 5 引入的标准并发工具包。面试话术JUC 是 Java 提供的一套开箱即用的并发编程工具覆盖了并发集合、锁、线程同步工具和线程池四大领域让我们不用自己造轮子就能写出线程安全的代码。二、并发集合线程安全的容器2.1 ConcurrentHashMap是什么线程安全的 HashMap取代HashTable和Collections.synchronizedMap()。演进Java 7分段锁Segment继承 ReentrantLock把 Map 分成 16 段锁粒度是段级别Java 8CAS synchronized锁粒度降到桶级别并发度更高且读操作基本无锁核心方法// 没有才放避免并发覆盖 map.putIfAbsent(key, value); // 计算并放值原子操作 map.computeIfAbsent(key, k - createValue(k)); // 安全遍历不会抛 ConcurrentModificationException for (Map.EntryK, V entry : map.entrySet()) { }面试话术ConcurrentHashMap 做了一层封装读操作不加锁写操作锁粒度很细Java 8 是桶级所以高并发场景下用它替代 HashTable 和 synchronizedMap性能能差十几倍。2.2 CopyOnWriteArrayList是什么线程安全的 ArrayList。原理写时复制——每次修改add/set/remove都会复制一份新数组写完之后让内部引用指向新数组。CopyOnWriteArrayListString list new CopyOnWriteArrayList();面试话术CopyOnWriteArrayList 适用于读多写少的场景比如黑名单、监听器列表。因为读操作完全不加锁写操作要复制数组所以写频繁的话内存开销大。2.3 BlockingQueue阻塞队列核心接口实现类实现类特点ArrayBlockingQueue有界数组实现公平锁可选LinkedBlockingQueue可选有界/无界链表实现PriorityBlockingQueue带优先级无界SynchronousQueue容量为 0直接交付DelayQueue延迟队列到期才能取LinkedTransferQueue有 transfer 语义核心方法4 种行为抛异常返回布尔阻塞等待超时等待入队add(e)offer(e)put(e)offer(e, time, unit)出队remove()poll()take()poll(time, unit)面试话术阻塞队列的核心是 put 和 take队列满时 put 会阻塞队列空时 take 会阻塞。最经典的应用就是线程池的任务队列生产者提交任务和消费者工作线程之间解耦。三、锁3.1 Lock 接口 vs synchronizedsynchronizedLock用法关键字自动获取释放手动 lock() / unlock()灵活性低只能同步块/方法高可跨方法尝试获取不行获取不到就阻塞tryLock()可尝试中断响应不支持lockInterruptibly()公平性非公平可配置公平/非公平条件等待wait() / notify()Condition.await() / signal()Lock lock new ReentrantLock(); lock.lock(); try { // 业务代码 } finally { lock.unlock(); // 必须手动释放 }面试话术synchronized 是 Java 关键字自动加解锁适合简单场景。Lock 是接口提供了 tryLock 尝试获取、lockInterruptibly 可中断等待、公平锁等更灵活的特性但必须手动 unlock通常放在 finally 块里。3.2 ReentrantLock可重入锁可重入同一个线程可以多次获取同一把锁不会死锁自己。示例ReentrantLock lock new ReentrantLock(true); // true 公平锁 lock.lock(); // 在这个锁保护下又可以调另一个也需要这个锁的方法 lock.lock(); lock.unlock(); lock.unlock(); // 加了几次就释放几次公平 vs 非公平非公平默认新线程来了会插队性能高但可能线程饿死公平FIFO 排队代价是吞吐量下降面试话术ReentrantLock 和 synchronized 都是可重入的。区别在于 ReentrantLock 可以指定公平锁还支持 tryLock 带超时等待、lockInterruptibly 响应中断。Java 6 之后 synchronized 做了大量优化锁升级、偏向锁简单场景两者性能差别不大。3.3 ReentrantReadWriteLock读写锁ReentrantReadWriteLock rw new ReentrantReadWriteLock(); rw.readLock().lock(); // 读锁多个线程可以同时读 rw.writeLock().lock(); // 写锁只有一个线程能写且写的时候不能读规则读读不互斥读写互斥写写互斥。面试话术读写锁适合读多写少的场景读锁可以被多个线程共享写锁是独占的。如果有大量并发读读写锁的吞吐量比普通锁高很多。3.4 StampedLockJava 8 引入读写锁的进一步优化支持乐观读。StampedLock sl new StampedLock(); long stamp sl.tryOptimisticRead(); // 乐观读不加锁先读 // 读取数据... if (!sl.validate(stamp)) { // 检查读数期间有没有被写 stamp sl.readLock(); // 被写了升级为悲观读 try { // 重新读 } finally { sl.unlockRead(stamp); } }面试话术StampedLock 比 ReadWriteLock 多了一个乐观读模式读操作先不加锁直接读读完之后验证一下有没有被写线程改过没改过就直接用这能让读操作完全无锁性能更高。四、同步工具类4.1 CountDownLatch倒计时器让一个线程等待其他 N 个线程完成后再继续。CountDownLatch latch new CountDownLatch(3); // 倒计时 3 次 // 线程 1/2/3 分别执行每个最后调 latch.countDown(); latch.countDown(); // 计数减 1 // 主线程等待所有任务完成 latch.await(); // 阻塞直到计数为 0 System.out.println(三个线程都干完了);特点一次性计数归零后不能再重置。面试话术CountDownLatch 像一个门闩计数器是 3就等 3 次 countDown 之后门闩打开。最经典场景是主线程等 N 个子线程都初始化完成后再继续。注意它只能用一次。4.2 CyclicBarrier循环栅栏N 个线程互相等待都到齐了再一起往下走。CyclicBarrier barrier new CyclicBarrier(3, () - { System.out.println(三个人都到了出发); }); // 每个线程 barrier.await(); // 等待其他人人数到了就执行回调然后放行特点可循环使用所有线程到达后自动重置。面试话术CyclicBarrier 像旅游团的集合点导游说人齐了就走所有人到齐了才能出发。比 CountDownLatch 多了一个可循环和回调功能。CountDownLatch 更像发令枪一个线程等其他人到齐了就开跑其他线程不用继续等。4.3 Semaphore信号量控制同时访问某个资源的线程数量。Semaphore semaphore new Semaphore(3); // 最多 3 个线程同时访问 semaphore.acquire(); // 获取许可证没有就阻塞 // 访问资源... semaphore.release(); // 释放许可证面试话术Semaphore 像停车场的道闸车位只有 3 个来一辆车放一辆满了就排队等。用在限流、连接池、数据库连接控制等场景。4.4 Exchanger交换器两个线程交换数据。ExchangerString exchanger new Exchanger(); // 线程 A String dataA exchanger.exchange(A的数据); // 此时 dataA 拿到了 B 的数据面试话术Exchanger 是线程间的数据交换点两个线程在同一个交换点交替换数据。用在遗传算法、校对工作等双方互换信息的场景。五、线程池5.1 为什么要用线程池不用线程池用线程池来一个任务 new 一个线程复用已创建好的线程线程无限创建OOM 风险控制最大并发数销毁线程开销大线程存活管理没法管理、监控统一管理5.2 ThreadPoolExecutor 七大参数public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 空闲线程存活时间 TimeUnit unit, // 时间单位 BlockingQueueRunnable workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )执行流程重要任务提交 │ ▼ 核心线程数满了 ├── 没满 → 创建核心线程执行 └── 满了 → 进入任务队列 │ ▼ 队列满了 ├── 没满 → 排队等待 └── 满了 → 创建非核心线程 │ ▼ 线程数达到 maximumPoolSize ├── 没到 → 创建临时线程执行 └── 到了 → 执行拒绝策略拒绝策略4 种策略行为AbortPolicy默认抛RejectedExecutionExceptionCallerRunsPolicy让提交任务的线程自己执行DiscardPolicy静默丢弃DiscardOldestPolicy丢弃队列中最旧的任务再尝试提交5.3 线程池的五种状态RUNNING → SHUTDOWN不接受新任务处理完队列任务 RUNNING → STOP不接受新任务不处理队列中断正在执行的 RUNNING/SHUTDOWN/STOP → TIDYING任务都执行完了 TIDYING → TERMINATEDterminated() 钩子执行完面试话术线程池状态控制比线程优先级高比如 SHUTDOWN 状态下即使有空闲核心线程也不会执行新任务。shutdown() 和 shutdownNow() 的区别前者不处理完队列不收兵后者暴力中断不等人。5.4 Executors 快捷创建线程池不推荐Executors.newFixedThreadPool(3); // 固定线程数队列 Integer.MAX_VALUE → 可能 OOM Executors.newCachedThreadPool(); // 无限创建线程 → 可能 OOM Executors.newSingleThreadExecutor();// 单线程 Executors.newScheduledThreadPool(3);// 定时任务面试话术阿里巴巴规范手册明确禁止使用 Executors 创建线程池因为它的默认参数有问题——newFixedThreadPool 用了无界队列Integer.MAX_VALUE任务积压可能 OOMnewCachedThreadPool 最大线程无限请求量大也 OOM。推荐自己 new ThreadPoolExecutor 显式传参。5.5 线程池大小如何设置CPU 密集型N 1N CPU 核数IO 密集型2N或更大取决于 IO 等待时间占比// 获取 CPU 核数 int cpuCores Runtime.getRuntime().availableProcessors();面试话术CPU 密集型任务线程数不宜超过 CPU 核数太多否则线程切换反而降低性能。IO 密集型任务大量时间在等磁盘/网络响应CPU 是空闲的可以多开线程把等待时间利用起来。六、常见面试题与回答话术6.1 基础题Q1synchronized 和 ReentrantLock 的区别话术synchronized 是关键字自动加解锁支持锁升级偏向→轻量→重量。ReentrantLock 是 API 级别的需要手动 unlock但提供了 tryLock、lockInterruptibly、公平锁等更灵活的功能。Java 6 之后 synchronized 做了大量优化简单场景两者性能差异不大。核心区别是 synchronized 自动省心ReentrantLock 灵活可控。Q2ConcurrentHashMap 的底层原理话术Java 8 的实现是数组链表红黑树用了 CAS synchronized 保证线程安全。put 时先对 key 做 hash 定位到桶如果桶为空就用 CAS 写入如果有冲突就锁住桶头节点遍历链表/红黑树。读操作完全不加锁靠 volatile 保证可见性。和 HashTable 比HashTable 是方法级锁并发度极低ConcurrentHashMap 的并发度是桶级别的。Q3volatile 关键字能保证什么话术volatile 保证两件事① 可见性——当一个线程修改了 volatile 变量其他线程立即可见不会读到自己线程的工作内存副本② 禁止指令重排序——防止 JVM 优化时把代码顺序调乱。但 volatile 不保证原子性比如 count 这种操作 volatile 也扛不住。Q4ThreadLocal 是什么有什么坑话术ThreadLocal 是线程本地变量每个线程都有自己的独立副本互不干扰。经典应用是 Spring 的事务管理把数据库连接放到 ThreadLocal 里。最大坑是内存泄漏——ThreadLocalMap 的 key 是弱引用GC 后 key 变 null但 value 是强引用还在如果不手动 remove整个线程池的线程一直活着就永远释放不了。所以每次用完后一定要调 remove()。Q5什么是 CAS有什么问题话术CASCompare And Swap是 CPU 原语指令比较并交换分三步① 读内存值 V② 和预期值 A 比较③ 如果相等就改成目标值 B不相等就重试。优点是没用锁性能好。有三个问题① ABA 问题加版本号解决AtomicStampedReference② 自旋时间太长 CPU 开销大③ 只能保证单个共享变量的原子操作。6.2 线程池专项Q6线程池的拒绝策略有哪些什么时候会触发话术四种策略——AbortPolicy 抛异常默认、CallerRunsPolicy 主线程自己跑、DiscardPolicy 静默丢、DiscardOldestPolicy 丢掉最旧任务再试。触发时机是核心线程满了 → 队列满了 → 最大线程也满了 → 新任务触发拒绝策略。Q7submit() 和 execute() 的区别话术execute 提交 Runnable没有返回值抛异常直接打印堆栈。submit 提交 Callable 或 Runnable返回 Future可以通过 future.get() 拿结果或捕获异常。submit 底层调的还是 execute。Q8如何合理设置线程池参数话术CPU 密集型设 N1IO 密集型设 2N 或更大。但实际生产中要看具体的业务指标任务平均耗时、QPS、可接受的响应时间通过压测调参。比如接口 TPS200每个任务耗时 50ms那理论上 10 个线程就够了。还可以用动态线程池美团开源方案运行时改参数。6.3 同步工具专项Q9CountDownLatch 和 CyclicBarrier 的区别话术CountDownLatch 是倒计时器一个线程等 N 个线程完事只能用一次。CyclicBarrier 是栅栏N 个线程互相等人到齐了就放行且可以循环使用。打个比方CountDownLatch 像是裁判的发令枪——所有运动员就位裁判扣扳机CyclicBarrier 像是跟团旅游——人齐了才发车下一站继续等人。Q10Semaphore 的应用场景话术Semaphore 做限流最合适比如限制接口的并发访问数为 50超过的排队或直接返回失败。也可以用来实现连接池acquire 时拿连接release 时归还。6.4 进阶题Q11AQS 是什么话术AQSAbstractQueuedSynchronizer是 JUC 的基石ReentrantLock、CountDownLatch、Semaphore 等同步器都基于它实现。核心是一个 statevolatile int和一个 CLH 双向队列等待线程队列。子类通过重写 tryAcquire/tryRelease 等方法控制 state 的获取和释放AQS 负责线程排队、阻塞、唤醒等底层机制。模板方法设计模式。Q12线程池的理想线程数和 CPU 核数的关系话术公式是线程数 CPU核数 × (1 等待时间/计算时间)。如果任务全是 CPU 计算等待时间≈0线程数≈CPU核数如果任务大量时间在等 IO等待时间远大于计算时间线程数就可以比 CPU 核数大很多。最终还是要通过压测验证。Q13如何排查线程池问题话术给线程池自定义名称通过 ThreadFactory这样出问题能快速定位是哪个池。监控核心指标活跃线程数、队列积压量、拒绝次数、任务执行耗时。可以用 ThreadPoolExecutor 的 beforeExecute/afterExecute 钩子方法做埋点也可以对接 Prometheus Grafana。6.5 代码题Q14手写一个线程池简要版面试官要的不是把 ThreadPoolExecutor 背一遍而是展示理解了核心设计。话术核心是生产者-消费者模式。一个阻塞队列存放任务一组工作线程从队列里取任务执行。需要一个 volatile 变量控制线程存活。支持 corePoolSize、maxPoolSize、keepAliveTime、workQueue。关键设计点工作线程在任务队列为空时阻塞等待take避免空转新任务先看核心线程池是否满再看队列是否满最后判断最大线程数。Q15如何实现一个延迟执行的任务话术用 ScheduledThreadPoolExecutor 或 DelayQueue。DelayQueue 的元素需要实现 Delayed 接口的 getDelay 方法队列内部按延迟时间排序。也可以用定时任务框架XXL-Job、Elastic-Job做分布式延迟任务。七、一句话快速记忆JUC 四大块并发集合、锁、同步器、线程池 三个集合ConcurrentHashMap高频、CopyOnWriteArrayList读多写少、BlockingQueue生产者消费者 三种锁ReentrantLock灵活、ReentrantReadWriteLock读写分离、StampedLock乐观读 四个同步器CountDownLatch等完事、CyclicBarrier等人齐、Semaphore限流、Exchanger换数据 线程池七大参数四种拒绝策略五种状态