多线程进阶核心知识详解(通俗版)
Java多线程进阶详解
一、锁策略:如何高效管理资源竞争
在多线程环境中,锁是协调资源访问的核心机制。不同的锁策略适用于不同的场景,理解它们的差异能帮助优化程序性能。
1. 乐观锁 vs 悲观锁
-
悲观锁:
-
核心思想:假设每次访问资源都会发生冲突,因此每次操作前先加锁。
-
类比:去图书馆借书时,管理员每次都锁上书柜,借阅者需排队等待钥匙。
-
实现:
synchronized
在竞争激烈时转为悲观锁,依赖操作系统的互斥锁(mutex)。 -
适用场景:写操作频繁,如银行转账、库存扣减。
-
-
乐观锁:
-
核心思想:假设冲突概率低,先操作再检查冲突。
-
类比:多人编辑在线文档,直接修改内容,提交时检测版本是否冲突。
-
实现:通过版本号或CAS(Compare and Swap)实现。
-
// 数据库乐观锁示例(伪SQL)
UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = current_version;
-
适用场景:读多写少,如点赞、评论计数。
2. 自旋锁 vs 挂起等待锁
-
自旋锁:
像追女神,每天发消息问“在吗?”,直到她分手。 -
原理:线程通过循环不断尝试获取锁,而非立即挂起。
-
优点:响应快,避免线程切换开销。
-
缺点:CPU空转,适合锁持有时间短的场景。
-
挂起等待锁:
被拒绝后默默等待,直到女神主动联系。 -
原理:线程获取锁失败后进入阻塞状态,等待被唤醒。
-
优点:节省CPU资源。
-
缺点:线程切换开销大,响应延迟。
-
实现:
synchronized
在竞争激烈时升级为重量级锁,依赖操作系统调度。
3. 公平锁 vs 非公平锁
-
公平锁:按请求顺序分配锁,避免线程饥饿。
-
实现:
ReentrantLock(true)
。 -
适用场景:需要严格顺序的业务,如排队系统。
-
-
非公平锁:允许插队,提高吞吐量。
-
实现:
synchronized
和默认的ReentrantLock
。 -
适用场景:高并发且任务执行时间差异大,如Web服务器。
-
4. 可重入锁(递归锁)
-
核心:同一线程可重复获取同一把锁,避免死锁。
-
实现:
synchronized
和ReentrantLock
。 -
示例:递归方法中的加锁操作。
-
二、CAS机制:无锁编程的魔法
CAS原理
-
三步操作:
-
读取内存值V。
-
比较V与预期值A。
-
若相等,将新值B写入V;否则重试。
-
1. 什么是CAS?
CAS = 检查并交换。就像自动售货机:
boolean CAS(当前值, 预期值, 新值) {if (当前值 == 预期值) {当前值 = 新值;return true;}return false;
}
2. 原子类实现原理
以AtomicInteger
为例:
// 伪代码实现i++
public int getAndIncrement() {int oldValue = value;while (!CAS(value, oldValue, oldValue+1)) {oldValue = value; // 值被改了,重试}return oldValue;
}
3. ABA问题与解决
:线程1读取值A,线程2将A→B→A,线程1的CAS操作误判无变化。
-
问题:你看到瓶子里的水还是满的,但其实已经被倒掉又装满了。
-
解决:加版本号,修改时检查值和版本号是否一致。
-
Java工具:
AtomicStampedReference
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
ref.compareAndSet(100, 200, 0, 1); // 检查值和版本号
三、synchronized的锁升级机制
JVM为优化synchronized
性能,设计了锁状态升级策略:
1. 无锁 → 偏向锁
-
场景:单线程访问,无竞争。
-
实现:在对象头记录线程ID,减少加锁开销。
-
类比:办公室的咖啡机默认归你使用,无需每次申请。
2. 偏向锁 → 轻量级锁
-
场景:多个线程交替访问,竞争较低。
-
实现:通过CAS自旋尝试获取锁,失败少量次后升级。
-
代码示例:
synchronized (obj) {// 轻量级锁通过CAS竞争 }
3. 轻量级锁 → 重量级锁
-
场景:高并发竞争,自旋消耗过多CPU。
-
实现:依赖操作系统的互斥锁(mutex),线程挂起等待。
-
缺点:用户态与内核态切换开销大。
4. 其他优化
-
锁消除:JVM检测到不可能存在竞争的锁,直接移除。
public String concat(String s1, String s2) {StringBuffer sb = new StringBuffer(); // 单线程下锁被消除sb.append(s1);sb.append(s2);return sb.toString(); }
锁粗化:合并多个相邻锁操作,减少加锁次数。
-
// 优化前 synchronized (obj) { doA(); } synchronized (obj) { doB(); } // 优化后 synchronized (obj) { doA(); doB(); }
四、JUC工具类:灵活控制并发
Java并发包(JUC)提供多种工具类,解决复杂并发问题。
1. ReentrantLock
-
核心功能:
-
可中断锁:
lockInterruptibly()
-
超时获取锁:
tryLock(long timeout, TimeUnit unit)
-
公平锁:
new ReentrantLock(true)
-
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {// 临界区代码
} finally {lock.unlock();
}
2. Semaphore(信号量)
-
作用:控制同时访问资源的线程数。
-
场景:数据库连接池限流。
-
示例:
-
Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问 semaphore.acquire(); // 获取许可 try {// 使用资源 } finally {semaphore.release(); // 释放许可 }
3. CountDownLatch(倒计时门闩)
-
作用:等待多个线程完成任务。
-
场景:启动服务前等待所有组件初始化完成。
-
示例:
-
CountDownLatch latch = new CountDownLatch(3); // 线程1、2、3分别执行任务后调用 latch.countDown(); latch.await(); // 主线程等待所有任务完成
4. CyclicBarrier(循环栅栏)
-
作用:多个线程相互等待,达到屏障后同时继续。
-
场景:多阶段任务并行处理。
-
示例:
-
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程到达屏障")); // 每个线程调用 barrier.await();
五、线程池:高效管理线程资源
线程池通过复用线程减少创建销毁开销,提升系统性能。
1. 核心参数
-
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, // 核心线程数(常驻)maximumPoolSize, // 最大线程数(核心+临时)keepAliveTime, // 临时线程空闲存活时间TimeUnit, // 时间单位workQueue, // 任务队列(如LinkedBlockingQueue)threadFactory, // 线程创建工厂rejectedExecutionHandler // 拒绝策略 );
2. 工作流程
-
提交任务,优先由核心线程执行。
-
核心线程满 → 任务入队列。
-
队列满 → 创建临时线程。
-
达到最大线程数 → 触发拒绝策略。
3. 拒绝策略
-
AbortPolicy:抛出
RejectedExecutionException
(默认)。 -
CallerRunsPolicy:由提交任务的线程执行。
-
DiscardOldestPolicy:丢弃队列中最老的任务。
-
DiscardPolicy:静默丢弃新任务。
4. 创建线程池的四种方式
// 固定线程数
ExecutorService fixedPool = Executors.newFixedThreadPool(10);
// 弹性线程数(无界队列)
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 单线程(任务顺序执行)
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 定时任务
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(5);
六、线程安全集合类
1. ConcurrentHashMap
-
Java7分段锁:将数据分为16段,每段独立加锁。
-
Java8优化:
-
每个桶独立加锁(链表头节点作为锁对象)。
-
链表长度≥8时转为红黑树,提高查询效率。
-
-
对比Hashtable:
特性 Hashtable ConcurrentHashMap 锁粒度 全表锁 桶级锁 并发性能 低 高 扩容机制 单线程扩容 多线程协同扩容
2. CopyOnWriteArrayList
-
原理:写操作时复制新数组,保证读操作无锁。
-
适用场景:读多写少(如监听器列表)。
-
代码示例:
-
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("item"); // 写时复制
七、死锁与解决方案
1. 死锁产生的四个条件
-
互斥:资源只能被一个线程占用。
-
不可抢占:资源只能由持有者释放。
-
请求保持:线程持有资源并等待其他资源。
-
循环等待:多个线程形成环形等待链。
2. 避免死锁的方法
-
统一加锁顺序:所有线程按固定顺序获取锁。
// 错误示例:线程1锁A→锁B,线程2锁B→锁A(可能死锁)
// 正确示例:所有线程按A→B顺序加锁
synchronized (lockA) {synchronized (lockB) { /* ... */ }
}
超时机制:使用tryLock
设置超时时间。
if (lock.tryLock(1, TimeUnit.SECONDS)) {try { /* ... */ } finally { lock.unlock(); }
}
3. 死锁检测与恢复
-
工具:使用
jstack
或JConsole分析线程转储。 -
恢复:强制终止线程或回滚操作(复杂且少用)。
总结
多线程编程的核心在于合理管理资源竞争与线程协作。关键点包括:
-
锁策略选择:根据场景选择乐观锁、悲观锁、公平锁等。
-
CAS无锁编程:通过原子类避免锁开销,注意ABA问题。
-
线程池优化:合理配置参数,平衡资源利用与响应速度。
-
安全集合:优先使用
ConcurrentHashMap
和CopyOnWriteArrayList
。 -
死锁预防:统一加锁顺序,使用超时机制。
通过深入理解这些机制,结合实际场景灵活运用,才能构建高效、稳定的并发程序