线程和同步性能

线程和同步性能

线程和硬件

  • 💡 CPU 增加线程并不能使应用程序性能倍增
  • 任务被提交到一个队列(可能不止一个队列),然后一定数量的线程会从队列中取出任务并执行它们
  • 线程池的大小对实现最佳性能至关重要
    • 在某些情况下,过大的线程池会导致性能急剧下降
    • 线程池大小应根据系统负载、CPU 和 I/O 任务比例来决定
    • 线程数超出 CPU 数量可能会降低吞吐量
    • 💡 CPU 不是瓶颈,外部资源是瓶颈时,增加的线程可能是有害的
    • 适用于 CPU 受限的计算(如矩阵计算)
    • 但不适用于 I/O 密集型应用(如需要频繁访问磁盘或发送大量 REST 请求)
    • 💡 线程池大小推荐
    • CPU 密集型任务: 线程数 = CPU 核心数 + 1
    • I/O 密集型任务: 线程数 = CPU 核心数 * (1 + I/O 等待时间 / 计算时间)

线程池

  • 💡 线程池的作用是将线程的管理交给框架,使其更易于维护
  • 线程池的核心组成:
    • 几乎所有的线程池 都包含一个 任务队列一组固定的线程
    • 任务从队列取出后,线程负责执行它
    • 💡 线程数量太多时
    • 线程上下文切换代价高,增加 CPU 负担
    • 任务队列中的任务需要排队,影响吞吐量
    • 💡 线程数量太少时
    • 任务执行太慢,应用程序响应变差
  • 💡 存在最优线程池大小
    • 计算方式: 线程池大小 = CPU 核心数 * 期望 CPU 使用率 * (1 + 平均等待时间 / 平均计算时间)
    • 需要根据实际场景调整,以平衡 CPU 计算任务和 I/O 任务的需求

ThreadPoolExecutor

队列类型

  • 无界队列
    • 任务的积累数量没有上限
    • 💡 由于队列是无界的,可能会导致任务堆积,最终耗尽内存
  • 有界队列
    • 用于平衡吞吐量和资源使用率
    • 💡 任务过载时,线程池可以作为第一道限流手段

线程池的配置建议

  • ✅ 适用于管理单调任务,其他情况请不要用
  • 💡 线程池的核心建议
    • 不要使用 Executors 直接创建线程池
    • 推荐直接使用 ThreadPoolExecutor
    • ArrayBlockingQueue 适用于高吞吐量但可控的任务积累

ForkJoinPool

  • 💡 适用于并行计算
  • 当任务不能拆分时,使用 ForkJoinPool 会退化为普通线程池
  • 💡 任务拆分与线程复用的核心
    • 任务会被拆分成更小的任务
    • 子任务可以由线程池中的其他线程执行
  • 💡 工作窃取策略
    • ForkJoinPool 允许线程从其他线程的任务队列中 偷取任务
    • 适用于 CPU 计算密集型应用
  • 💡 线程池大小
    • 适用于计算任务时,推荐:-Djava.util.concurrent.ForkJoinPool.common.parallelism=NN = 核心数

✅ 结论

  • 创建太多线程会降低性能
  • 合理的线程池配置可以极大提升吞吐量

线程同步的性能优化

同步

  • synchronized 提供了内置的同步锁
  • ReentrantLock 提供更高级的同步控制
  • CAS (Compare and Swap) 算法 用于无锁同步
  • java.util.concurrent.atomic 包提供原子操作类
  • 💡 过多线程同步可能会导致性能下降,特别是 CPU 计算密集型进程

同步的代价

  • 阿姆达尔定律(Amdahl's Law)
    • 计算并行度受限于串行部分
    • 并行化程度越低,收益越少
    • 💡 线程同步对 CPU 计算密集型任务影响更大
  • 获取锁的开销
    • 线程切换带来的 CPU 负担
    • synchronized 可能会膨胀成重量级锁
    • 轻量级锁(CAS 方式)适用于低竞争场景
  • 锁的升级
    • 偏向锁轻量级锁重量级锁
    • CAS 适用于短时间竞争的场景,避免不必要的锁升级

线程同步优化

  • 减少锁的粒度
    • 使用局部变量,避免共享数据
    • 仅在需要同步的地方加锁
  • 无锁优化
    • 使用 CAS 替代锁
    • AtomicInteger 等原子类减少锁竞争
    • 适用于 CPU 计算密集型任务
  • 避免伪共享(false sharing)
    • 共享缓存行导致 CPU 性能下降
    • 💡 使用 @Contended 注解减少伪共享
  • 优化锁竞争
    • 适当增加锁的粒度
    • 尽量使用并发容器,如 ConcurrentHashMap
    • 读多写少的场景,使用 StampedLock

伪共享问题

  • cache line sharing
    • 多个线程修改同一缓存行中的数据,导致 CPU 缓存失效
    • 解决方案:
    • 使用 @Contended 注解
    • 使用 padding 让变量独占缓存行
  • 影响
    • 可能会导致严重的性能下降
    • Java 8 之后提供 @Contended 解决方案

@Contended 注解

  • 减少伪共享,提高并行计算效率
  • 需要 -XX:-RestrictContended 选项才能生效
  • 适用于高并发场景
  • Java 11 之后 @Contended 仅限 java.base 相关类

💡 结论

  • 减少锁的使用,尽量使用无锁并发
  • 优化锁的粒度,避免全局锁
  • 减少伪共享,优化 CPU 缓存使用

JVM 线程优化

  • 当空间不足时,可以调整线程使用的内存
  • 每个线程都有一个原生栈,操作系统会在这里存储线程的调用栈信息
    • 32 位的 Windows JVM 原生栈大小是 320KB
    • 原生栈的大小是 1MB
    • 在 64 位的 JVM 中,通常不会修改这个值,除非机器的物理内存相当紧张
    • 许多程序可以在栈大小为 256KB 时运行
    • ★较小的栈大小可以防止应用程序用完原生内存
    • 很少有程序需要用到完整的 1MB
  • -Xss=N 标志:改变线程的栈大小
    • 在 32 位的 JVM 中,进程使用的内存达到了 4GB (或者小于 4GB,取决于操作系统) 的最大大小
    • ★减小栈的大小可以解决无法从 JVM 的异常中判断是这三种情况中的哪一种
    • ●系统实际已经耗尽虚拟内存
  • ★减小栈的大小可以解决无法从 JVM 的异常中判断是这三种情况中的哪一种

原生内存溢出

  • 在 Unix 风格的系统上,用户创建的进程 (他们正在运行的所有程序) 已经达到了此次登录配置的最大进程数
  • 单个线程被认为是一个进程

偏向锁

  • 如果一个线程最近使用了某个锁,那么下次它执行被同一个锁保护的代码时,处理器的缓存更有可能还包含该线程需要的数据
  • 让锁偏向于最近访问锁的线程
  • ★但是偏向锁需要簿记信息,所以有时机器性能会更糟糕
  • 使用线程池的应用程序 (包括某些应用程序和 REST 服务器) 在偏向锁生效时,往往表现得更差
  • 禁用偏向锁:-XX:-UseBiasedLocking
  • ★默认开启的

线程优先级

  • 每个 Java 线程都有一个由开发人员定义的优先级,这是对操作系统的一种提示,用来说明程序认为特定线程有多重要
  • 操作系统会为机器上运行的每个线程计算一个当前优先级
    • 当前优先级既考虑了 Java 分配的优先级,也考虑了许多其他因素
    • ●最重要的因素是线程上次运行到现在有多长时间
  • 无论其优先级如何,都不会有线程因为等待访问 CPU 而“饥饿”
  • Windows 上,Java 优先级较高的线程往往比优先级较低的线程运行得更多,但即使是低优先级的线程也会获得相当多的 CPU 时间
  • ●★无论在哪种情况下,都不能依赖线程的优先级来决定它的运行频率
  • ●★如果某些任务比其他任务更重要,就必须使用应用程序逻辑来确定它们的优先级

监控线程和锁

  • 线程的总数
    • 通过系统提供的基本线程信息可以大致了解运行的线程数量
    • 确保它不会太高或太低
  • 查看线程信息可以确定线程被阻塞的原因
    • ●它们在等待资源
    • ●它们在等待 I/O
  • 查看线程
    • jconsole: 显示 JVM 内的线程状态
    • 可以查看 JVM 内部,并能在底层知道线程何时被阻塞
    • Java 飞行记录器 JFR: ★提供了一个简单的方法来检查导致线程阻塞的事件
  • 查看阻塞线程
    • 提供虚拟机中每个线程的状态信息,包括线程是否在运行、是否在等待锁、是否在等待 I/O
    • jstack: ★在一定程度上可以查看线程阻塞在什么资源上
    • 大量的阻塞线程会降低性能。不管是什么原因造成的阻塞,都需要改变配置或应用程序,以避免阻塞

线程性能没有太多可以优化

  • ●可以调整的 JVM 标志相对较少
  • ●这些标志的效果也十分有限
  • 良好的线程性能最佳实践准则
    • 管理线程数
    • 限制同步影响
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
javaJDKjvm

JDK 8 到 JDK 23 的 ​语法糖(语言特性)​ 和 ​垃圾回收(GC)优化

2025-3-12 17:41:55

javaJDKjvm

生产OOM排查

2025-3-21 8:51:45

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧