简介:本文系统梳理Java并发编程的知识脉络,从java.util.concurrent整体架构出发,深入解析锁框架、原子类、并发集合、线程池与同步工具五大模块的设计原理与使用要点,并结合典型代码示例与高频面试问题,帮助读者构建完整的并发知识体系,有效提升面试表现与工程实战能力。
在多核处理器已成为标配的今天,能否正确、高效地编写并发程序,直接决定了系统能否充分利用硬件资源。然而并发编程同时也是Java面试中区分候选人水平的关键领域:它不仅考查记忆性的知识点,更考查对内存模型、线程调度、锁机制等底层原理的理解深度。
很多开发者的学习困境在于知识点零散——今天背一个synchronized的锁升级,明天记一个线程池参数,始终无法形成体系。要突破这一困境,最佳切入点是把握java.util.concurrent(以下简称JUC)包的整体架构,再逐个模块深入。下面我们按照“总—分”的顺序展开。
从设计角度看,JUC包可以划分为五个层次分明的模块,它们共同构成了Java并发工具箱:
java.util.concurrent├── locks(锁框架)│ ├── Lock 接口│ │ ├── ReentrantLock│ │ └── ReentrantReadWriteLock│ ├── Condition(条件队列)│ └── AbstractQueuedSynchronizer(AQS 同步器骨架)├── atomic(原子类)│ ├── AtomicInteger / AtomicLong│ ├── AtomicReference│ └── AtomicStampedReference├── collections(并发集合)│ ├── ConcurrentHashMap│ ├── CopyOnWriteArrayList│ └── BlockingQueue 接口及其实现(ArrayBlockingQueue、│ LinkedBlockingQueue、PriorityBlockingQueue 等)├── executors(线程池)│ ├── Executor 接口│ ├── ExecutorService 接口│ ├── ThreadPoolExecutor│ ├── ScheduledThreadPoolExecutor│ └── Executors 工具类└── tools(同步工具)├── CountDownLatch├── CyclicBarrier├── Semaphore└── Exchanger
理解这张架构图有两层意义:其一,面试中回答“谈谈你对JUC的了解”时,能从全局视角切入,展示系统性思维;其二,日常开发中遇到并发问题时,可以快速定位应使用哪一层工具——需要互斥找锁框架,需要无锁计数找原子类,需要线程安全容器找并发集合,需要任务调度找线程池,需要线程协作找同步工具。
AbstractQueuedSynchronizer(AQS)是JUC中绝大多数同步器的底层骨架,其核心设计可以概括为三点:
面试中若被问到“ReentrantLock的加锁流程”,本质上就是在考AQS:先通过CAS修改state,成功则获取锁;失败则入队阻塞,等待前驱节点唤醒后重试。
典型用法如下,务必将unlock放入finally块,防止异常导致锁永不释放:
public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();public void method() {lock.lock();try {// 临界区代码} finally {lock.unlock();}}}
它的核心特性包括:
与synchronized相比,两者都是可重入互斥锁,synchronized由JVM层面实现、使用简单且经过持续优化;ReentrantLock则是API层面的实现,功能更丰富(公平性、可中断、可超时、多条件变量Condition),但要求开发者手动释放锁。面试时应能根据场景说明取舍依据,而非简单说“哪个更好”。
public class ReadWriteLockExample {private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private final Map<String, Object> cache = new HashMap<>();public Object get(String key) {rwLock.readLock().lock();try {return cache.get(key);} finally {rwLock.readLock().unlock();}}public void put(String key, Object value) {rwLock.writeLock().lock();try {cache.put(key, value);} finally {rwLock.writeLock().unlock();}}}
其设计思想是按访问模式细化锁粒度:
需要补充的高频考点是锁降级:写线程可以先释放写锁前获取读锁(写锁降级为读锁),从而保证自己刚写入的数据对后续读操作可见,同时允许其他读线程进入。反之,锁升级(持读锁直接获取写锁)不被支持,容易造成死锁。
原子类的底层依赖处理器的Compare-And-Swap指令:CAS需要三个操作数——内存地址V、期望值E、新值N,仅当V处的当前值等于E时才将其更新为N,否则失败。整个过程由硬件保证原子性,无需加锁,因此也称“乐观锁”或“无锁编程”。
CAS并非完美,面试常考它的三个弱点:
AtomicInteger、AtomicLong适合计数器、序列号生成;AtomicReference适合以对象为单位的原子更新;AtomicIntegerFieldUpdater等反射式更新器则能在不改动类定义的前提下为字段提供原子访问。
针对ABA问题,AtomicStampedReference在引用之外额外维护一个版本号戳,每次修改版本号递增,CAS时同时比对引用与版本号,从根源上识别“值被改过又改回来”的情况。这是面试中“如何解决ABA问题”的标准答案。
ConcurrentHashMap是面试出镜率最高的并发集合。要点在于:早期版本采用分段锁设计,将整表划分为多个段,不同段可并发写入;后续版本放弃了分段,改为对桶头节点加锁的细粒度方案,空桶插入用CAS完成,冲突桶才用synchronized锁住头节点,锁粒度进一步缩小。此外,size的统计采用了类似分片计数的设计以减少竞争,且迭代器为弱一致性,不抛出并发修改异常。
CopyOnWriteArrayList采用写时复制:读操作完全无锁直接读当前数组;写操作先复制一份新数组,在新数组上修改后原子替换引用。它适合读极多、写极少且数据量不大的场景(如监听器列表、白名单),缺点是写开销大且只能保证最终一致性。
BlockingQueue家族以“阻塞”为核心语义:队列满时生产者阻塞、队列空时消费者阻塞,天然适合解耦生产者与消费者。ArrayBlockingQueue基于定长数组、单锁实现;LinkedBlockingQueue基于链表、默认近乎无界且读写分离两把锁,吞吐更高但需警惕内存溢出;PriorityBlockingQueue则按优先级出队。线程池的任务缓冲正是构建在BlockingQueue之上,两者应对照学习。
频繁创建销毁线程的开销巨大,且无限制的线程数量会导致内存耗尽与过度上下文切换。线程池通过复用线程、控制并发度,将“任务”与“线程”解耦,是服务端程序稳定性的基石。
ThreadPoolExecutor的核心构造参数包括:核心线程数corePoolSize、最大线程数maximumPoolSize、空闲存活时间keepAliveTime及其单位、任务队列workQueue、线程工厂threadFactory、拒绝策略handler。
任务提交后的处理顺序是高频考题,务必牢记:
四种内置拒绝策略为:直接抛异常中止、由提交线程自己执行(实现简单的负反馈限流)、静默丢弃新任务、丢弃队首最老任务。
参数设置的经验法则:CPU密集型任务线程数接近核心数加一,IO密集型可适当放大,但最终应以压测数据为准,而非死记公式。实际工程中不建议使用Executors的快捷工厂方法直接创建线程池,因为其默认的无界队列或超大线程数可能带来内存溢出与资源耗尽风险,推荐显式new ThreadPoolExecutor并明确每个参数。
这四个工具的共同底层正是前文提到的AQS:CountDownLatch与Semaphore基于共享模式,ReentrantLock基于独占模式。理解了AQS,整个同步工具家族就不再是孤立的知识点,而是一套设计模式的多种应用。
面对并发面试题,建议按以下路径构建知识网络:
并发编程的深度决定了天花板的高度。当你不再逐题背诵,而是能从架构设计、底层原理、实践规范三个层面自如展开时,任何并发面试题都会成为展示知识体系的舞台。