【Java并发编程】概念与核心问题、线程核心、并发控制、线程池、并发容器、并发问题
文章目录
- 前言
- 1. 并发编程基础:概念与核心问题
- 1.1 核心概念辨析
- 1.2 并发编程核心问题(3大根源)
- 2. 线程核心:生命周期与操作
- 2.1 线程生命周期(JDK 8+ 6种状态)
- 2.2 线程核心操作(创建与控制)
- 2.2.1 线程创建3种方式
- 2.2.2 线程控制常用方法
- 3. 并发控制核心技术:锁与同步
- 3.1 synchronized:内置锁(隐式锁)
- 3.1.1 用法场景
- 3.1.2 核心特性
- 3.2 Lock:显式锁(JUC包)
- 3.2.1 标准用法(必须在finally中释放锁)
- 3.2.2 高级特性
- 3.3 锁对比与选型
- 4. 线程池:线程管理的最佳实践
- 4.1 线程池核心原理与参数
- 4.1.1 核心参数(7个)
- 4.1.2 4种拒绝策略
- 4.2 常见线程池对比(Executors工具类)
- 4.3 线程池实战注意事项
- 5. 并发容器:线程安全的数据存储
- 6. 原子类与CAS:无锁并发控制
- 6.1 原子类分类与示例
- 6.1.1 代码示例(AtomicInteger)
- 6.2 CAS核心原理
- 7. 线程通信:协同与同步机制
- 8. 常见并发问题与解决方案
- 8.1 死锁(Deadlock)
- 8.2 线程安全问题(数据错乱)
- 8.3 上下文切换开销
- 9. 并发编程实战选型指南
- 10. 总结
前言
若对您有帮助的话,请点赞收藏加关注哦,您的关注是我持续创作的动力!有问题请私信或联系邮箱:funian.gm@gmail.com
在多核CPU时代,“充分利用硬件资源提升程序效率”是开发核心需求。单线程程序无法发挥多核优势,而Java并发编程通过多线程协同执行,实现“同一时间处理多个任务”,广泛应用于服务器、中间件等高频场景。但并发也带来线程安全、死锁等问题,需通过标准化工具与规范解决。本文基于JDK 8+,拆解Java并发编程的核心原理与实战方案。
1. 并发编程基础:概念与核心问题
1.1 核心概念辨析
概念 | 定义 | 举例 |
---|---|---|
并发(Concurrency) | 同一时间段内,多个任务交替执行(单核CPU通过时间片切换实现) | 单CPU上多线程处理请求 |
并行(Parallelism) | 同一时刻,多个任务同时执行(依赖多核CPU) | 多核CPU上多线程同时计算 |
线程(Thread) | 进程内的执行单元,共享进程资源,轻量级(切换开销远低于进程) | Java程序的main方法对应主线程 |
进程(Process) | 操作系统资源分配的基本单位,进程间资源独立,重量级 | 一个Java.exe对应一个进程 |
线程安全 | 多线程并发访问时,程序仍能保证数据正确性、执行结果一致 | ConcurrentHashMap的put操作 |
1.2 并发编程核心问题(3大根源)
并发问题本质是“多线程对共享资源的无序访问”,根源可归结为3点:
- 原子性:一个操作或多个操作,要么全部执行且不被打断,要么全不执行(如
i++
实际是read→modify→write
三步,非原子); - 可见性:一个线程修改共享变量后,其他线程能立即看到修改结果(CPU缓存导致修改暂存本地,未同步到主内存);
- 有序性:程序执行顺序与代码顺序一致(JVM指令重排序优化可能打乱顺序,如
int a=0; boolean flag=false;
可能先执行flag=true
)。
2. 线程核心:生命周期与操作
2.1 线程生命周期(JDK 8+ 6种状态)
Java线程生命周期由Thread.State
枚举定义,状态转换如下:
graph LRA[新建(NEW)] --> B[就绪(RUNNABLE)]B --> C[运行(RUNNING)]C --> D[阻塞(BLOCKED)]C --> E[等待(WAITING)]C --> F[超时等待(TIMED_WAITING)]D --> BE --> BF --> BC --> G[终止(TERMINATED)]
状态 | 含义 | 进入方式 | 退出方式 |
---|---|---|---|
NEW | 线程已创建,未调用start() | new Thread() | 调用start() |
RUNNABLE | 线程就绪(等待CPU调度)或正在运行(CPU执行中) | start() 、阻塞/等待状态结束 | CPU调度执行、进入阻塞/等待状态 |
BLOCKED | 线程等待锁(如synchronized未获取到锁) | 竞争synchronized锁失败 | 获取到synchronized锁 |
WAITING | 线程无限期等待(需其他线程唤醒) | wait() 、join() 、LockSupport.park() | notify() 、notifyAll() 、LockSupport.unpark() |
TIMED_WAITING | 线程超时等待(到时间自动唤醒) | sleep(ms) 、wait(ms) 、join(ms) | 超时时间到、被唤醒 |
TERMINATED | 线程执行完成或异常终止 | run()执行完、抛出未捕获异常 | 无(线程生命周期结束) |
2.2 线程核心操作(创建与控制)
2.2.1 线程创建3种方式
创建方式 | 核心原理 | 优点 | 缺点 | 代码示例 |
---|---|---|---|---|
继承Thread类 | 重写run() 方法,直接操作线程对象 | 实现简单,可直接访问Thread的方法 | 单继承限制,代码耦合高 | class MyThread extends Thread { public void run() {} } new MyThread().start(); |
实现Runnable接口 | 重写run() ,传入Thread构造器 | 无单继承限制,代码解耦 | 无法直接获取线程执行结果(需额外处理) | Runnable runnable = () -> {}; new Thread(runnable).start(); |
实现Callable接口 | 重写call() ,结合FutureTask获取结果 | 可获取执行结果,支持异常抛出 | 实现稍复杂,需FutureTask包装 | Callable<Integer> callable = () -> 1; FutureTask<Integer> ft = new FutureTask<>(callable); new Thread(ft).start(); ft.get(); |
2.2.2 线程控制常用方法
方法名 | 作用 | 注意事项 |
---|---|---|
start() | 启动线程,将线程状态从NEW转为RUNNABLE(JVM调用run() ) | 不可重复调用(重复调用抛IllegalThreadStateException) |
run() | 线程执行逻辑(业务代码) | 直接调用仅为普通方法,不会启动新线程 |
sleep(long ms) | 让线程进入TIMED_WAITING,不释放锁(synchronized/Lock) | 需捕获InterruptedException |
wait() | 让线程进入WAITING,释放synchronized锁(仅在同步块中调用) | 需配合notify() 唤醒,需捕获InterruptedException |
join() | 等待当前线程执行完成(如主线程等待子线程结束) | 需捕获InterruptedException |
interrupt() | 中断线程(设置中断标志,不直接终止线程) | 需通过isInterrupted() 判断中断状态 |
3. 并发控制核心技术:锁与同步
控制并发的核心是“保证共享资源的有序访问”,Java提供内置锁(synchronized) 与显式锁(Lock) 两类方案。
3.1 synchronized:内置锁(隐式锁)
synchronized是Java原生锁,基于“对象监视器(Monitor)”实现,无需手动释放,属于可重入、非公平锁。
3.1.1 用法场景
锁定场景 | 核心逻辑 | 代码示例 |
---|---|---|
锁定实例方法 | 锁对象为当前类实例(this) | public synchronized void method() {} |
锁定静态方法 | 锁对象为当前类的Class对象 | public static synchronized void method() {} |
锁定代码块 | 锁对象为自定义对象(如Object) | synchronized (lockObj) { // 业务逻辑 } |
3.1.2 核心特性
- 可重入:同一线程可多次获取同一把锁(如同步方法调用同步方法,不会死锁);
- 隐式释放:线程退出同步块/方法时,JVM自动释放锁(即使抛出异常);
- 非公平:线程获取锁的顺序不保证(新线程可能优先于等待线程获取锁)。
3.2 Lock:显式锁(JUC包)
java.util.concurrent.locks.Lock
是JDK 5引入的显式锁,需手动lock()
获取锁、unlock()
释放锁,支持更灵活的并发控制(如公平锁、超时获取)。核心实现类是ReentrantLock
(可重入锁)。
3.2.1 标准用法(必须在finally中释放锁)
Lock lock = new ReentrantLock(); // 默认非公平锁,可传true设为公平锁
try {lock.lock(); // 获取锁(阻塞直到获取成功)// 业务逻辑(操作共享资源)
} finally {lock.unlock(); // 释放锁(避免异常导致锁泄漏)
}
3.2.2 高级特性
- 公平锁支持:构造器传
true
,线程按等待顺序获取锁(避免饥饿,但性能略低); - 超时获取锁:
tryLock(long time, TimeUnit unit)
,超时未获取返回false(避免死等); - 可中断获取:
lockInterruptibly()
,获取锁时可被interrupt()
中断; - 条件变量:通过
newCondition()
获取Condition对象,实现精细化线程通信(如生产者-消费者)。
3.3 锁对比与选型
对比维度 | synchronized | ReentrantLock |
---|---|---|
锁获取/释放 | 隐式(JVM自动) | 显式(手动lock()/unlock()) |
公平性 | 仅非公平锁 | 支持公平/非公平(构造器指定) |
超时获取 | 不支持 | 支持(tryLock(ms)) |
中断支持 | 不支持 | 支持(lockInterruptibly()) |
条件变量 | 仅1个(通过wait()/notify()) | 多个(通过newCondition()) |
锁状态查询 | 不支持 | 支持(isLocked()、hasQueuedThreads()) |
性能(高并发) | JDK 6+优化后与ReentrantLock接近 | 略高(灵活控制锁粒度) |
适用场景 | 简单同步场景(如单方法/代码块) | 复杂场景(公平锁、超时、多条件通信) |
4. 线程池:线程管理的最佳实践
频繁创建/销毁线程会产生大量开销(上下文切换、内存占用),线程池通过“预先创建线程、复用线程”降低开销,是Java并发编程的“标配工具”。
4.1 线程池核心原理与参数
Java线程池核心类是ThreadPoolExecutor
,其工作流程:
- 线程池初始化时创建核心线程;
- 任务提交时,优先使用核心线程执行;
- 核心线程满时,任务存入阻塞队列;
- 队列满时,创建非核心线程执行;
- 总线程数达最大线程数且队列满时,触发拒绝策略。
4.1.1 核心参数(7个)
参数名 | 含义 | 示例值 | 作用说明 |
---|---|---|---|
corePoolSize | 核心线程数(线程池长期维持的线程数) | 5 | 任务提交时优先使用核心线程,即使空闲也不销毁(除非allowCoreThreadTimeOut=true) |
maximumPoolSize | 最大线程数(核心线程+非核心线程的总数上限) | 10 | 阻塞队列满时,最多可创建的线程数 |
keepAliveTime | 非核心线程空闲时间(超时后销毁) | 60L | 释放空闲资源,避免内存浪费 |
unit | keepAliveTime的时间单位 | TimeUnit.SECONDS | 配合keepAliveTime使用 |
workQueue | 阻塞队列(存储等待执行的任务) | LinkedBlockingQueue | 缓冲任务,避免线程数骤增 |
threadFactory | 线程工厂(创建线程的工具类) | Executors.defaultThreadFactory() | 统一设置线程名称、优先级等 |
handler | 拒绝策略(任务无法执行时的处理方式) | AbortPolicy | 保护线程池,避免任务无限堆积 |
4.1.2 4种拒绝策略
拒绝策略类 | 核心逻辑 | 适用场景 |
---|---|---|
AbortPolicy(默认) | 抛出RejectedExecutionException | 严格场景(不允许任务丢失,需感知异常) |
CallerRunsPolicy | 由提交任务的线程(如主线程)执行 | 并发量低,允许主线程临时处理任务 |
DiscardPolicy | 直接丢弃任务,不抛异常 | 非核心任务(如日志收集,允许丢失) |
DiscardOldestPolicy | 丢弃队列中最旧的任务,再提交新任务 | 任务有优先级,新任务比旧任务重要 |
4.2 常见线程池对比(Executors工具类)
Executors
提供4种默认线程池,但部分存在隐患(如OOM),需谨慎使用。
线程池类型 | 核心参数配置 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
FixedThreadPool | corePoolSize=maximumPoolSize,队列无界 | 线程数固定,避免线程膨胀 | 队列无界(LinkedBlockingQueue),任务过多易OOM | 固定并发量的场景(如服务器接收请求) |
CachedThreadPool | corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,队列同步移交 | 线程自动扩容/回收,灵活适应任务量 | 最大线程数无界,高并发易创建过多线程导致OOM | 短期、轻量级任务(如临时计算) |
ScheduledThreadPool | corePoolSize固定,支持定时/延迟执行 | 支持定时任务,线程复用 | 长期定时任务需注意线程泄漏 | 定时任务(如定时清理缓存、日志归档) |
SingleThreadExecutor | corePoolSize=maximumPoolSize=1,队列无界 | 单线程执行,保证任务顺序 | 队列无界,任务过多易OOM;单线程故障影响所有任务 | 需顺序执行的任务(如文件写入、单线程消费队列) |
重要建议:避免使用Executors默认线程池,优先手动创建
ThreadPoolExecutor
,明确核心参数(尤其是阻塞队列容量),避免OOM风险。
4.3 线程池实战注意事项
- 合理设置核心参数:
- CPU密集型任务(如计算):核心线程数=CPU核心数+1(减少上下文切换);
- I/O密集型任务(如DB查询、网络请求):核心线程数=CPU核心数×2(利用CPU空闲时间);
- 避免线程泄漏:确保任务中无无限循环、死锁,避免线程长期占用;
- 监控线程池状态:通过
getActiveCount()
(活跃线程数)、getQueue().size()
(队列任务数)监控,及时调整参数; - 优雅关闭线程池:用
shutdown()
(等待任务执行完)或shutdownNow()
(强制中断任务),避免直接kill进程。
5. 并发容器:线程安全的数据存储
普通容器(如ArrayList、HashMap)线程不安全,JUC包提供并发容器,解决多线程下的数据安全问题。
并发容器类 | 对应普通容器 | 核心实现原理 | 线程安全级别 | 适用场景 |
---|---|---|---|---|
ConcurrentHashMap | HashMap | JDK 8+:CAS+synchronized(分段锁优化) | 读写并发安全 | 高并发键值对存储(如缓存、配置) |
CopyOnWriteArrayList | ArrayList | 读写分离(写时复制数组,读不加锁) | 读多写少安全 | 读频繁、写极少的场景(如配置列表) |
CopyOnWriteArraySet | HashSet | 基于CopyOnWriteArrayList实现 | 读多写少安全 | 读频繁的去重场景 |
ConcurrentLinkedQueue | LinkedList(队列) | 无锁CAS实现(链表结构) | 高并发安全 | 无界并发队列(如任务传递) |
ArrayBlockingQueue | 数组队列 | ReentrantLock+Condition实现 | 高并发安全 | 有界队列(如生产者-消费者模型) |
LinkedBlockingQueue | 链表队列 | ReentrantLock+Condition实现 | 高并发安全 | 可配置有界/无界队列(如线程池任务队列) |
对比普通容器:并发容器通过“锁优化(如分段锁)、无锁CAS、读写分离”实现线程安全,性能远高于
Collections.synchronizedXXX()
包装的普通容器。
6. 原子类与CAS:无锁并发控制
原子类基于“CAS(Compare and Swap,比较并交换)”实现无锁并发控制,避免锁的上下文切换开销,适用于简单共享变量的原子操作。
6.1 原子类分类与示例
原子类类别 | 核心类 | 功能示例 | 适用场景 |
---|---|---|---|
原子基本类型 | AtomicInteger、AtomicLong | incrementAndGet() (原子自增)、getAndSet() | 计数器(如接口调用次数、任务完成数) |
原子引用类型 | AtomicReference、AtomicStampedReference | compareAndSet() (原子更新引用) | 原子更新对象引用(如原子修改POJO属性) |
原子数组 | AtomicIntegerArray、AtomicLongArray | getAndAdd(int i, int delta) (原子更新数组元素) | 原子操作数组元素(如并发修改数组计数) |
原子字段更新器 | AtomicIntegerFieldUpdater | updateAndGet(obj, func) (原子更新对象字段) | 不修改类源码,原子更新对象的非静态字段 |
6.1.1 代码示例(AtomicInteger)
public class AtomicDemo {private static final AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {// 多线程原子自增Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {count.incrementAndGet(); // 原子i++,替代非原子的count++}});Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {count.incrementAndGet();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get()); // 输出:2000(无线程安全问题)}
}
6.2 CAS核心原理
CAS是一种乐观锁机制,核心逻辑:
- 输入参数:内存地址V、预期值A、新值B;
- 执行逻辑:若内存地址V的值等于预期值A,则将V的值更新为B,返回true;否则不更新,返回false;
- 循环重试:若CAS失败,线程自旋重试(无锁等待,避免上下文切换)。
CAS问题:
- ABA问题:内存值从A→B→A,CAS误判为未修改(解决方案:AtomicStampedReference加版本号);
- 自旋开销:高并发下CAS频繁失败,自旋导致CPU占用过高(解决方案:限制自旋次数、搭配锁);
- 只能原子操作单个变量:需原子操作多个变量时,需用
AtomicReference
包装对象。
7. 线程通信:协同与同步机制
多线程需通过“通信”实现协同(如生产者生产后通知消费者消费),Java提供5种核心通信方式。
通信方式 | 核心类/方法 | 原理 | 适用场景 | 代码示例核心片段 |
---|---|---|---|---|
wait()/notify() | Object类方法(需配合synchronized) | 线程等待时释放锁,唤醒后重新竞争锁 | 简单的线程间通知(如单生产者-单消费者) | synchronized (lock) { lock.wait(); } / lock.notify(); |
Condition | Lock的newCondition() | 基于Lock实现,支持多条件分组通知 | 复杂通知(如多生产者-多消费者,分组唤醒) | Condition cond = lock.newCondition(); cond.await(); cond.signal(); |
CountDownLatch | JUC包类(倒计时门闩) | 计数器减至0时,唤醒所有等待线程 | 等待多个线程完成(如主线程等所有子线程执行完) | CountDownLatch latch = new CountDownLatch(2); latch.countDown(); latch.await(); |
CyclicBarrier | JUC包类(循环栅栏) | 等待指定数量线程到达栅栏后,共同执行 | 多线程分阶段执行(如所有线程准备好后开始计算) | CyclicBarrier barrier = new CyclicBarrier(2); barrier.await(); |
Semaphore | JUC包类(信号量) | 控制同时访问资源的线程数(许可证机制) | 限流(如控制并发访问数据库的线程数) | Semaphore sem = new Semaphore(5); sem.acquire(); sem.release(); |
8. 常见并发问题与解决方案
8.1 死锁(Deadlock)
- 产生条件:资源互斥、持有并等待、不可剥夺、循环等待;
- 解决方案:
- 破坏循环等待:按固定顺序获取锁(如按锁对象的hashCode排序);
- 破坏持有并等待:一次性获取所有锁(如用
Lock.tryLock()
批量获取); - 定时释放锁:用
tryLock(ms)
避免无限等待; - 监控死锁:通过
jstack <PID>
命令查看线程堆栈,定位死锁线程。
8.2 线程安全问题(数据错乱)
- 产生原因:共享变量未加同步,违反原子性/可见性/有序性;
- 解决方案:
- 用synchronized/Lock保证原子性与可见性;
- 用volatile修饰共享变量(保证可见性与有序性,不保证原子性);
- 用原子类(AtomicXXX)实现原子操作;
- 用并发容器替代普通容器。
8.3 上下文切换开销
- 产生原因:CPU切换线程执行时,需保存/恢复线程状态(如程序计数器、寄存器);
- 解决方案:
- 合理设置线程池大小,避免线程过多;
- 用无锁编程(CAS、原子类)减少锁竞争;
- 用协程(如Project Loom的VirtualThread)替代线程,降低切换开销。
9. 并发编程实战选型指南
业务需求 | 推荐技术 | 排除技术 |
---|---|---|
简单同步代码块/方法 | synchronized | ReentrantLock(过度设计) |
复杂同步(公平锁、多条件) | ReentrantLock+Condition | synchronized(功能不足) |
高并发线程管理 | 手动创建ThreadPoolExecutor | Executors默认线程池(易OOM) |
读多写少的列表存储 | CopyOnWriteArrayList | ArrayList(线程不安全)、Vector(性能低) |
高并发键值对存储 | ConcurrentHashMap | HashMap(线程不安全)、Hashtable(性能低) |
简单计数器 | AtomicInteger | synchronized(开销高) |
等待多个线程完成 | CountDownLatch | join()(无法复用,灵活性低) |
限流控制 | Semaphore | 手动计数(易出错) |
10. 总结
Java并发编程的核心是“在利用多核优势的同时,保证线程安全与效率”,关键要点可总结为:
- 基础核心:理解线程生命周期、原子性/可见性/有序性三大问题;
- 同步工具:synchronized(简单场景)、Lock(复杂场景)、原子类(无锁场景);
- 线程管理:线程池是最佳实践,需手动配置核心参数,避免默认实现;
- 数据存储:并发容器(ConcurrentHashMap、CopyOnWriteArrayList)替代普通容器;
- 线程通信:根据场景选wait/notify、Condition、CountDownLatch等工具;
- 问题排查:用jstack/jconsole监控线程状态,定位死锁、线程安全问题。
掌握Java并发编程,不仅能提升程序性能,更能应对高并发场景(如服务器、中间件)的开发需求,是Java高级工程师的核心技能之一。