多线程
多线程
Runnable 和 Callable 有什么区别
- Runnable 的 run 方法没有返回值, Callable 的 call 方法有
- Callable 的 call 方法是个泛型
- Runnable 的 run 方法不能抛异常, Callable 的 call 方法可以
wait 和 sleep 方法有什么区别
- 归属不同: wait 方法是 Object 类中的, sleep 方法是 Thread 的静态方法
- 醒来时机不同: sleep(long) 和 wait(long) 都会在等待相应毫秒后醒来, wait 方法可以被 notify 唤醒
- 锁特性不同: wait 方法必须配合Synchronized 使用, sleep 无此限制
- wait 方法执行后会释放锁对象, 允许其他线程获得锁(我放弃 CPU, 你们可以用)
- sleep 如果在 Synchronized 代码块中执行, 不会释放锁对象(我放弃 CPU, 你们也不能用)
Synchronized 底层原理
ConcurrentHashMap
- 在 1.7 版本中 ConcurrentHashMap 是用 数组 + 链表的数据结构实现的
- 数组又分大数组 Segment 和小数组 HashEntry (大数组可以理解为 MySQL 的数据库, 小数组可以理解为表, 每个数组可以存储多条数据, 用链表连接)
- 它是基于 ReentrantLock 实现加锁和释放锁的操作, 锁的粒度为 Segment
- 在 1.8 版本中用的是 数组 + 链表 + 红黑树的数据结构实现的
- 用的是 CAS + volatile 或 Synchronized 方式保证线程安全
- 添加元素首先会判断容器是否为空, 如果为空则用 volatile + CAS 初始化
- 容器不为空则根据存储的元素计算位置是否为空, 为空用 CAS 设置节点
- 不为空则使用 Synchronized 加锁
JMM 内存结构
介绍一下volatile 关键字
- 是用来修饰变量的
- 被其修饰的变量在修改后可以立即同步到主内存, 每次使用之前都是从主内存读取, 因此 volatile 可以保证可见性
- volatile 可以禁止指令重排序, 被 volatile 修饰的变量操作, 会严格按照代码顺序执行
CAS
AQS
- 抽象队列同步器
- 内部维护了一个先进先出的双向队列, 队列中存储的是排队的线程
- AQS 内部有一个属性 state, 相当于资源, 默认是 0, 当有一个线程成功修改 state 为 1, 则当前线程就等于获取了资源
- 对 state 修改的时候用 CAS 操作, 保证多个线程修改情况下的原子性
ThreadLocal
线程池参数
用线程池有什么好处
- 不用反复创建收回线程所需要的资源
- 系统的响应速度更快
- 更加合理利用 CPU 资源
- 可以统一管理资源
线程池的创建方法
- ThreadPoolExecutor 构造函数
- Executors 工具类
什么时候使用线程池
- 当需要频繁创建和销毁线程的时候
- 常量池的参数配置
- 快速响应用户请求, 用户发起试试请求, 服务追求响应时间, 比如一个用户要查看商品信息, 那么响应越快越好, 所以这种场景不应该设置队列去缓存并发任务, 调高 corePoolSize 和 maxPoolSize 尽可能创造多的线程执行任务
- 批处理操作, 离线计算大量任务, 比如统计报表, 这种情况下, 任务量巨大不需要瞬时完成, 也就是吞吐量优先, 所以应该设置队列去缓冲并发任务
notify 和 wait
- wait 和 notify 均依赖于锁, 且锁的对象必须是同一个对象, 否则无法执行唤醒
- notify 唤醒是随机唤醒一个线程, 唤醒的范为是同一锁对象, 所有 wait 的线程
notify 同一依赖于锁, 必须在同步快中执行, 执行之后会立刻释放锁吗?
- notify 在执行后不会立即唤醒, 而是等到 notify 同步块执行完之后才会去执行唤醒
wait/notify 和 await/signal 关系
- wait/notify
- 基于 Synchronized 实现的
- 无法控制唤醒谁, 随机唤醒
- await/signal
- 基于 Lock 实现的
- 使用 Condition 对象可以细粒度低控制线程的等待和唤醒
Executors 工具类提供的四个线程池子
newSingleThreadExecutor()
用于需要保证任务按顺序执行的场景- corePoolSize: 1
- maximumPoolSize: 1
- 阻塞队列: LinkedBlockingQueue 大小是 Integer.MAX_VALUE
newFixedThreadPool(int nThreads)
用于负载稳定、任务量恒定的场景- corePoolSize: nThreads
- maximumPoolSize: nThreads
- 阻塞队列: LinkedBlockingQueue 大小是 Integer.MAX_VALUE
newCachedThreadPool()
适用于执行大量短期异步任务的场景,能高效地利用系统资源- corePoolSize: 0
- maximumPoolSize: Integer.MAX_VALUE
- 阻塞队列: 大小为 0
ScheduledThreadPoolExecutor(int corePoolSize)
用于需要定时或周期性执行任务的场景- corePoolSize: 0
- maximumPoolSize: Integer.MAX_VALUE
- 阻塞队列: 大小无界, 按照延迟的时间长短对任务进行排序
shutdown() 和 shutdownNow()
- shutdown(): 关闭线程池, 线程池的状态变为 SHUTDOWN, 线程池不再接受新任务, 但是队列里的任务得执行完毕
- shutdownNow(): 关闭线程池, 线程池的状态变为 STOP, 线程池会终止当前正在运行的任务, 并停止处理排队的任务并返回正在等待执行的 List
isTerminated() 和 isShutdown()
- isShutdown(): 当调用 shutdown() 方法后返回 true
- isTerminated: 当调用 shutdown() 方法后, 并执行完所有提交的任务后返回 true
如何正确使用线程池
- 正确声明线程池, 用 ThreadExecutorPool 构造函数声明, 而不是 Executors 工具类(会有 OOM 的风险)
- 监测线程池运行状态, SpringBoot 的 Actuator 组件
- 建议不同业务使用不同的线程池
- 给线程池命名
- 别忘记关闭线程池
- 线程池尽量不要放耗时任务, 耗时任务用消息队列异步执行
动态调整线程池大小(美团)
- 用 setCorePoolSize 调整核心线程池大小
- 用 setMaximumPoolSize 调整最大线程池大小
- 重写 LinkedBlockingQueue, 把里面的 capacity 字段的 final关键字去掉, 变为可变的
动态调整线程池大小(Nacos)
@RefreshScope
支持 Nacos 动态刷新@Value("${max.size}"
读取在 Nacos 配置的具体信息- 配置监听, Nacos 配置变更时实时修改线程池配置
线程池中线程异常后, 销毁还是复用?
- 使用 execute() 提交任务
- 如果异常没有在任务内捕获, 那么该异常会导致当前线程终止, 控制台打印日志
- 线程池会检测到这种异常终止, 并创建一个新的线程来替换它, 从而保证配置的线程数不变
- 使用 submit() 提交任务
- 如果在执行中发生异常, 会封装在 submit() 返回的
Future
对象中 - 当调用
Future.get()
方法可以捕获到一个ExecutionException
- 线程不会因异常终止, 会继续存在线程池中
- 如果在执行中发生异常, 会封装在 submit() 返回的
CompletableFuture
- 是用来做多个任务的编排的, 规定任务前后执行顺序
- Future 不支持异步任务的编排, 获取计算结果的
get()
方法为阻塞调用
Java 不同锁的实现和使用场景
synchronized
关键字- 可重入锁
- 非公平锁
ReentrantLock
- 可重入锁
- AQS 队列
- 可以实现公平锁
ReadWrite Lock
读写锁- 读锁是共享锁, 读读不互斥, 获取到读锁的时候, 无法获取写锁
- 写锁是独占锁, 加上写锁的时候别的线程读写都不行
CountDownLatch 类(倒计时器)
- CountDownLatch 允许 count 个线程阻塞在一个地方, 直至所有线程的任务都执行完毕
- 典型用法
- 某一个线程在开始运行前等待 n 个线程执行完毕: 将 CountDownLatch 的计数器初始化为 n, 每当一个任务线程执行完毕, 就将计数器 -1, 当计数器的值变为 0 时, 在 CountDownLatch 上 await() 的线程就被唤醒
- 典型应用场景就是启动一个服务时, 主线程需要等待多个组件加载完成, 之后再继续执行
- 实现多个线程开始执行任务的最大并发性: 强调的是多个线程在某一时刻同时开始执行, 类似于赛跑, 将多个线程放到起点, 同时开跑, 初始化一个共享的 CountDownLatch 对象, 将其计数器初始化为 1, 多个线程在开始执行任务前首先
countdownLathch.await()
当主线程调用 countDown() 时, 计数器变为 0, 多个线程被同时唤醒
- 某一个线程在开始运行前等待 n 个线程执行完毕: 将 CountDownLatch 的计数器初始化为 n, 每当一个任务线程执行完毕, 就将计数器 -1, 当计数器的值变为 0 时, 在 CountDownLatch 上 await() 的线程就被唤醒
如何预防死锁
- 破坏请求与保持条件: 一次性申请所有资源
- 破坏不可剥夺条件: 占用部分资源的线程去申请其他资源时, 如果申请不到, 主动释放他所占用的资源
- 破坏循环等待条件: 按照顺序申请资源, 释放资源反序释放
java 多线程通信
- 锁
wait/notify
volatile
- 管道
join
ThreadLocal
如何检测死锁
jstack
命令可以查看 JVM 线程栈和堆内存的情况, 如果有死锁, 通常会输出Found one Java-level deadlock
字样- 实际项目可以用
top
查看 CPU 占用情况, 出现死锁会导致 CPU 内存占用过高
锁升级
- 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
多线程
http://showyoubug.cn/2024/05/26/多线程/