多线程

多线程

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
    • 线程不会因异常终止, 会继续存在线程池中

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, 多个线程被同时唤醒

如何预防死锁

  • 破坏请求与保持条件: 一次性申请所有资源
  • 破坏不可剥夺条件: 占用部分资源的线程去申请其他资源时, 如果申请不到, 主动释放他所占用的资源
  • 破坏循环等待条件: 按照顺序申请资源, 释放资源反序释放

java 多线程通信

  • wait/notify
  • volatile
  • 管道
  • join
  • ThreadLocal

如何检测死锁

  • jstack 命令可以查看 JVM 线程栈和堆内存的情况, 如果有死锁, 通常会输出 Found one Java-level deadlock 字样
  • 实际项目可以用 top 查看 CPU 占用情况, 出现死锁会导致 CPU 内存占用过高

锁升级

  • 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率

多线程
http://showyoubug.cn/2024/05/26/多线程/
作者
Dong Su
发布于
2024年5月26日
许可协议