带你深入了解更高级的多线程知识,包括各种锁策略、CAS机制、synchronized原理、JUC工具类等核心内容。这些知识是成为Java高级开发者的必经之路,也是面试中经常考察的重点。
1. 常见的锁策略
乐观锁 vs 悲观锁
这是两种截然不同的并发控制思路:
-
悲观锁 :总是假设最坏情况,每次访问共享资源前都会加锁。比喻是:同学A认为老师很忙,会先发消息确认老师是否有空(加锁操作),得到肯定答复后才去问问题。典型的实现是
synchronized和ReentrantLock。 -
乐观锁:假设冲突不常发生,访问数据时不加锁,只在更新时检查是否有冲突。比喻是:同学B认为老师很闲,直接去找老师问问题。如果老师确实忙,就下次再来。典型实现是CAS机制。
适用场景:
-
锁竞争激烈时,悲观锁更合适
-
锁竞争不激烈时,乐观锁效率更高
重量级锁 vs 轻量级锁
-
重量级锁 :依赖操作系统提供的
mutex互斥锁实现,涉及大量内核态/用户态切换,容易引发线程调度,成本较高。 -
轻量级锁 :尽量在用户态完成加锁操作,减少系统调用开销。
synchronized开始是轻量级锁,冲突严重时升级为重量级锁。
内核态 vs 用户态的比喻:在银行窗口外自己办理业务是用户态(效率可控),在窗口内由工作人员办理是内核态(效率不可控)。
自旋锁
传统锁在获取失败时,线程会进入阻塞状态,放弃CPU。自旋锁采用不同策略:
// 自旋锁伪代码
while(抢锁(lock) == 失败) {}
特点:
-
优点:不放弃CPU,一旦锁释放能立即获取
-
缺点:如果锁持有时间长,会持续消耗CPU资源
比喻:追求女神时,死皮赖脸每天问候(自旋锁) vs 陷入沉沦很久后再尝试(挂起等待锁)。
公平锁 vs 非公平锁
-
公平锁:遵守"先来后到",按请求顺序分配锁
-
非公平锁:不按顺序,允许插队
synchronized是非公平锁。公平锁需要额外数据结构记录线程顺序,可能降低吞吐量。
可重入锁 vs 不可重入锁
-
可重入锁:允许同一线程多次获取同一把锁
-
不可重入锁:不允许,会导致死锁
"把自己锁死"的场景:线程持有锁后再次尝试获取同一把锁,如果是不可重入锁就会死锁。Java中的synchronized和ReentrantLock都是可重入锁。
读写锁
针对"读多写少"场景的优化锁:
-
读锁与读锁:不互斥
-
写锁与写锁:互斥
-
读锁与写锁:互斥
Java的ReentrantReadWriteLock实现了读写锁。举例:教务系统中,查看同学列表(读操作)频繁,修改同学列表(写操作)不频繁。
2. CAS(Compare and Swap)
什么是CAS
CAS是一种无锁编程技术,包含三个操作:
-
比较内存值V与预期值A
-
如果相等,将新值B写入V
-
返回操作是否成功
// CAS伪代码
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
CAS是原子的硬件指令,可视为乐观锁的一种实现。
CAS的应用
1. 实现原子类
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.getAndIncrement(); // 线程安全的i++
2. 实现自旋锁
基于CAS可以实现更灵活的自旋锁。
ABA问题
问题描述:
线程t1读取值A,准备改为Z。在此期间,t2将值从A改为B又改回A。t1的CAS操作会成功,但无法感知中间的变化。
解决方案:引入版本号
-
每次修改版本号+1
-
CAS比较值和版本号
-
Java提供
AtomicStampedReference解决此问题
"翻新手机"的比喻:无法区分是全新手机还是翻新后又恢复原样的手机。
3. synchronized原理
synchronized的特性(JDK 1.8)
-
开始是乐观锁,冲突频繁时转为悲观锁
-
开始是轻量级锁,持有时间长时转为重量级锁
-
轻量级锁实现用自旋锁策略
-
非公平锁
-
可重入锁
-
不是读写锁
加锁工作过程
JVM将锁状态分为四级,逐步升级:
1. 偏向锁
-
第一个加锁线程进入偏向状态
-
只是做标记,不真正加锁
-
后续无竞争则避免加锁开销
-
有竞争时取消偏向,进入轻量级锁
2. 轻量级锁
-
通过CAS实现
-
竞争不激烈时使用
-
自适应自旋:根据情况调整自旋次数
3. 重量级锁
-
竞争激烈时使用
-
依赖操作系统
mutex -
涉及内核态切换,成本高
其他优化
锁消除:JVM检测到不可能存在共享数据竞争时,消除不必要的锁。
锁粗化:将多次连续的加锁解锁合并为一次,减少开销。
4. JUC(java.util.concurrent)常见类
Callable接口
与Runnable相比,Callable可以有返回值和抛出异常。
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get(); // 阻塞等待结果
ReentrantLock
可重入互斥锁,比synchronized更灵活:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// working
} finally {
lock.unlock(); // 必须手动释放
}
与synchronized的区别:
-
手动释放锁
-
可尝试获取锁(
tryLock) -
可配置公平锁
-
更精确的等待-唤醒机制
原子类
基于CAS实现的高性能原子操作类:
-
AtomicInteger、AtomicLong、AtomicBoolean等 -
性能远高于加锁实现
线程池
核心参数理解(文档用"开公司"比喻):
-
corePoolSize:正式员工数(永不辞退) -
maximumPoolSize:正式员工+临时工数 -
keepAliveTime:临时工空闲时间 -
workQueue:任务队列 -
RejectedExecutionHandler:拒绝策略
四种拒绝策略:
-
AbortPolicy:抛出异常 -
CallerRunsPolicy:调用者执行 -
DiscardOldestPolicy:丢弃最老任务 -
DiscardPolicy:丢弃新任务
信号量(Semaphore)
控制同时访问特定资源的线程数量:
Semaphore semaphore = new Semaphore(4); // 4个可用资源
semaphore.acquire(); // 申请资源(P操作)
// 访问资源
semaphore.release(); // 释放资源(V操作)
比喻:停车场车位展示牌。
CountDownLatch
等待多个任务完成:
CountDownLatch latch = new CountDownLatch(10);
// 每个任务完成后调用
latch.countDown();
// 主线程等待
latch.await();
比喻:跑步比赛,所有选手到达终点才公布成绩。
5. 线程安全的集合类
CopyOnWriteArrayList
写时复制的线程安全List:
-
写操作复制新数组,不影响读操作
-
读多写少时性能高
-
读操作不需要加锁
ConcurrentHashMap
线程安全的HashMap,相比Hashtable的优化:
-
锁粒度更细:锁每个桶(链表头节点),而非整个表
-
CAS优化 :
size等属性用CAS更新 -
扩容优化:化整为零,多线程协助扩容
-
结构优化:链表过长转红黑树(Java 8)
6. 死锁
死锁产生的四个必要条件
-
互斥使用:资源不能共享
-
不可抢占:不能强制夺取资源
-
请求和保持:持有资源同时请求新资源
-
循环等待:形成等待环路
如何避免死锁
最实用的是破坏循环等待:按固定顺序获取锁。
// 错误:可能死锁
线程1:lock1 -> lock2
线程2:lock2 -> lock1
// 正确:按固定顺序
线程1:lock1 -> lock2
线程2:lock1 -> lock2
"吃饺子需要酱油和醋"的生动比喻说明了死锁场景。
7. 常见面试题解析
最后列出了多个高频面试题:
-
volatile关键字的用法:保证内存可见性,不保证原子性
-
Java多线程数据共享:通过堆内存共享数据
-
线程状态转换:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
-
ConcurrentHashMap的优化:锁分段->锁桶、链表转红黑树、协助扩容
-
进程和线程的区别:资源分配 vs 调度单位、内存空间是否共享
总结
Java多线程进阶涉及的知识点既深且广。从基础的锁策略到CAS机制,从synchronized的内部原理到JUC工具类的使用,每一部分都需要深入理解。关键要点:
-
理解不同锁策略的适用场景,没有绝对的优劣
-
掌握CAS的原理和局限性,特别是ABA问题
-
了解synchronized的优化过程,从偏向锁到重量级锁
-
熟练使用JUC工具类,根据场景选择合适工具
-
重视线程安全问题,使用线程安全集合或手动同步
-
避免死锁,按顺序获取锁
多线程编程如同走钢丝,需要在性能和正确性之间找到平衡。希望这篇博客能帮助你在多线程的道路上走得更稳更远。在实践中不断尝试和思考,你会逐渐掌握这门艺术。