多线程与并发编程
Java 多线程与并发编程
很多人学并发:
- 一上来就背八股
- synchronized
- CAS
- AQS
- volatile
- JUC
结果:
背了一堆。
问:
“为什么会线程不安全?”
直接卡死。
其实并发编程最重要的是:
建立整体认知。
这篇从:
- 线程
- 锁
- CAS
- AQS
- volatile
- ThreadLocal
- 线程池
一路讲到面试常问。
尽量人话。
一、线程与进程的区别
1. 资源分配和调度单位
进程:
是操作系统资源分配的基本单位。
线程:
是 CPU 调度的基本单位。
简单理解:
进程 = 公司 线程 = 员工一个进程里:
可以有多个线程。
2. 地址空间
不同进程:
内存隔离。
每个进程都有自己的:
- 堆
- 栈
- 页表
- 虚拟内存
线程:
共享进程资源。
包括:
- 堆
- 方法区
- 全局变量
但线程有自己的:
- 栈
- 程序计数器
- 寄存器
3. 通信方式
进程通信:
需要 IPC:
- 管道
- socket
- 消息队列
- 共享内存
线程:
因为共享内存。
直接读变量即可。
4. 创建和切换开销
进程:
切换时:
需要切换地址空间。
开销大。
线程:
共享地址空间。
切换轻很多。
5. 稳定性
进程之间互相隔离。
一个进程崩:
一般不会影响别的进程。
线程:
共享内存。
一个线程乱写:
整个进程都可能寄。
二、为什么进程创建和销毁开销大?
1. 独立虚拟内存
每个进程都需要:
- 页表
- 堆
- 栈
- PCB
创建时:
都要重新初始化。
2. 地址空间切换
进程切换:
需要:
- 切页表
- 刷TLB
- 切MMU映射
线程很多时候不需要。
3. 资源回收复杂
进程结束:
需要回收:
- 文件
- socket
- 锁
- 内存
三、平台线程与虚拟线程
1. 平台线程
传统 Java 线程。
1:1 对应 OS线程。
Java Thread = OS Thread特点:
- 重
- 栈内存大
- 创建数量有限
2. 虚拟线程
Java21 正式推出。
虚拟线程:
是 JVM 管理的轻量线程。
适合:
高并发 IO 场景。
原理
传统线程:
线程执行IO ↓ 线程阻塞 ↓ OS线程被占住虚拟线程:
虚拟线程执行IO ↓ JVM挂起虚拟线程 ↓ 平台线程继续执行别的任务所以:
虚拟线程本质:
是减少 OS线程浪费。
四、Java线程创建方式
1. 继承 Thread
classMyThreadextendsThread{publicvoidrun(){}}2. Runnable
推荐。
classTaskimplementsRunnable{publicvoidrun(){}}3. Callable + FutureTask
支持:
- 返回值
- 异常
4. 线程池
实际开发最常用。
五、线程状态
1. NEW
未启动。
2. RUNNABLE
运行或等待CPU。
3. BLOCKED
等待锁。
4. WAITING
无限等待。
5. TIMED_WAITING
限时等待。
6. TERMINATED
结束。
六、sleep 和 wait 的区别
1. sleep 不释放锁
2. wait 释放锁
3. wait 必须在 synchronized 中调用
否则:
IllegalMonitorStateException4. wait 需要 notify 唤醒
七、线程通信方式
1. 共享变量
2. volatile
保证:
- 可见性
不保证:
- 原子性
3. wait / notify
线程协作。
4. BlockingQueue
生产者消费者核心。
八、为什么会线程不安全?
本质:
多个线程同时修改共享资源。
比如:
count++实际:
读取 修改 写回不是原子操作。
九、synchronized详解
这是 Java 最经典的锁。
1. synchronized怎么用?
修饰普通方法
publicsynchronizedvoidtest(){}锁的是:
当前对象。
即:
this修饰静态方法
publicstaticsynchronizedvoidtest(){}锁的是:
Class对象修饰代码块
synchronized(lock){}锁指定对象。
2. synchronized底层原理
本质:
对象监视器:
MonitorJVM通过:
monitorenter monitorexit实现加锁解锁。
3. synchronized支持重入吗?
支持。
synchronized(this){synchronized(this){}}同线程可重复获得锁。
4. synchronized 是公平锁吗?
不是。
属于:
非公平锁。
5. synchronized 可以锁字符串吗?
可以。
但不推荐。
比如:
synchronized("abc")字符串常量池可能导致:
莫名锁竞争。
抽象的一批。
6. synchronized 锁升级过程
JDK6后优化很大。
锁会升级:
无锁 ↓ 偏向锁 ↓ 轻量级锁 ↓ 重量级锁偏向锁
默认偏向第一个线程。
减少CAS。
轻量级锁
线程竞争不激烈。
采用:
CAS自旋。
重量级锁
竞争严重。
线程阻塞。
OS介入。
7. JVM对synchronized做了什么优化?
锁粗化
多个连续加锁:
合并。
锁消除
JIT发现:
没有竞争。
直接去掉锁。
自旋锁
避免线程立即阻塞。
偏向锁
减少无竞争成本。
十、volatile详解
1. volatile有什么作用?
保证:
- 可见性
- 有序性
不保证:
- 原子性
2. 什么是可见性?
线程修改变量后。
别的线程立刻能看到。
3. 什么是有序性?
防止指令重排序。
4. volatile如何实现?
底层:
内存屏障。
5. volatile能保证线程安全吗?
不能。
count++仍然线程不安全。
6. volatile 和 synchronized 区别
| 对比 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 性能 | 高 | 较低 |
十一、CAS详解
1. 什么是CAS?
Compare And Swap。
比较并交换。
CPU原子指令。
2. CAS流程
V = A ? 是: 修改成B 否: 失败重试3. CAS有什么问题?
ABA问题
值:
A → B → ACAS会认为没变化。
自旋开销大
一直失败会疯狂循环。
只能保证单变量原子性
4. 如何解决ABA问题?
使用:
AtomicStampedReference加版本号。
十二、AQS详解
AQS:
AbstractQueuedSynchronizerJUC核心。
1. AQS是什么?
一个:
队列同步器。
很多锁都基于它:
- ReentrantLock
- CountDownLatch
- Semaphore
2. AQS核心思想
维护:
volatileintstate通过:
CAS修改state。
失败线程:
进入队列等待。
3. AQS底层结构
CLH双向队列。
十三、ReentrantLock
1. ReentrantLock 和 synchronized区别
ReentrantLock功能更多
支持:
- 公平锁
- 可中断
- 超时
- Condition
synchronized更简单
JVM级别优化。
2. 公平锁和非公平锁
公平锁
先来先得。
非公平锁
允许插队。
吞吐量更高。
十四、ThreadLocal
1. ThreadLocal是什么?
线程局部变量。
每个线程:
独立副本。
2. ThreadLocal原理
每个线程内部:
有:
ThreadLocalMap3. 为什么key是弱引用?
避免:
ThreadLocal无法回收。
4. 为什么还会内存泄漏?
key被回收:
value可能还在。
如果线程池线程不结束:
value可能一直存在。
5. 如何避免?
使用后:
remove()十五、线程池详解
这是面试高频。
1. 为什么使用线程池?
避免:
频繁创建销毁线程。
2. ThreadPoolExecutor核心参数
corePoolSize
核心线程数。
maximumPoolSize
最大线程数。
keepAliveTime
空闲线程存活时间。
workQueue
任务队列。
threadFactory
线程工厂。
rejectHandler
拒绝策略。
3. 线程池执行流程
任务来了 ↓ 核心线程没满 → 创建核心线程 ↓ 核心线程满 → 进入队列 ↓ 队列满 → 创建非核心线程 ↓ 线程也满 → 拒绝策略4. 拒绝策略有哪些?
AbortPolicy
默认。
直接抛异常。
CallerRunsPolicy
调用者自己执行。
DiscardPolicy
直接丢弃。
DiscardOldestPolicy
丢弃最旧任务。
5. 为什么不推荐 Executors?
因为:
可能导致:
OOM。
比如:
newFixedThreadPool队列:
无界。
6. 线程池参数怎么设置?
CPU密集型:
CPU核数 + 1IO密集型:
CPU核数 * 2或者更高。
十六、死锁
1. 什么是死锁?
多个线程互相等待资源。
谁也不释放。
2. 死锁四个条件
互斥
请求保持
不可剥夺
循环等待
3. 如何避免死锁?
固定加锁顺序
超时机制
减少锁嵌套
十七、并发容器
1. ConcurrentHashMap
JDK8:
- CAS
- synchronized
实现。
2. CopyOnWriteArrayList
读写分离。
适合:
读多写少。
3. BlockingQueue
阻塞队列。
线程池核心。
十八、悲观锁和乐观锁
1. 悲观锁
认为:
一定会冲突。
直接加锁。
比如:
synchronized ReentrantLock2. 乐观锁
认为:
不一定冲突。
CAS重试。
十九、总结
并发编程核心:
其实就三件事:
1. 资源共享
多个线程如何共享数据。
2. 线程调度
CPU怎么切换线程。
3. 数据安全
如何避免:
- 数据错乱
- 死锁
- 可见性问题
很多高级并发框架。
底层逃不过:
- CAS
- AQS
- volatile
- synchronized
理解这些。
后面看源码才不会像看天书。
