线程和同步性能
线程和硬件
- 💡 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=N
(N = 核心数
)
- 适用于计算任务时,推荐:
✅ 结论
- 创建太多线程会降低性能
- 合理的线程池配置可以极大提升吞吐量
线程同步的性能优化
同步
- 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 标志相对较少
- ●这些标志的效果也十分有限
- 良好的线程性能最佳实践准则
- 管理线程数
- 限制同步影响
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。