/ 👨‍💻 代码敲不完 / 14浏览

Java 程序优化之-如何更好的利用CPU

昨天,有人跟我聊起项目中对程序的优化,有一个特别有意思的话题《如何榨干一台机器的 CPU》

现在的市面上,多核 CPU 是主流,有了多核的加持,可以更加有效的发挥硬件的能力,基于 Java 程序,我们究竟该如何更加有效的应用多核的能力?我个人经验来讲,主要考虑一下几个方面:

  • 并行执行任务

  • 减少共享数据的写操作

  • 采用合适的方式处理竞争资源

  • 减少数据拷贝次数

  • 合适的 GC

接下来详细说明。

1. 并行执行任务

合理利用多线程执行任务,可以有效的发挥 CPU 的多核性能。由于超线程技术的存在,实际上 CPU 可以并行执行的线程数量通常是物理核心数量的 2 倍。

我们都知道,在计算机中,进程是操作系统资源(内存、显卡、磁盘)分配的最小单位。线程是 CPU 执行调度的最小单位。

因此,实现并行计算的方式大体上有三种:多进程、多线程、多进程 + 多线程。具体采用哪种方式,就需要实际情况实际分析了。整体指导方针是:如果多线程可以解决,就不要尝试引入多进程。因为每个进程之间是独立的,多进程任务难免会涉及到进程之间通信,而进程之间的协调与通信通常会比较复杂。容易为程序引入额外的复杂度,得不偿失。

2. 减少共享数据的写操作

深入到线程中,每个线程都有自己的内存空间,在这个内存中,线程可以随意进行读写。因此多线程任务中,提高效率的优化手段之一就是:

尽量避免多个线程共同操作共享资源,如果条件允许,尽量采用以空间换时间的方式,将数据复制多份保存在每个线程单独的内存空间中。

如果必须存在共享内存的操作,我们的措施通常是,尽量减少共享数据的写操作,在共享内存中,多个线程的读操作是不存在资源的竞争的。一旦涉及到写共享内存,通常会使用 volatile 关键字保证内存数据对多个线程的可见性,这种情况下就不可避免的要涉及到插入内存屏障指令,用来保证处理器对指令的执行顺序不会打乱。相比不存在内存屏障的操作,性能会有所下降。

因此,需要尽量减少多个线程对共享内存的写操作。具体的方案是:

  • 通过业务逻辑控制,在程序设计之初,排除掉共享数据的方案

  • 在每个线程内部创建单独的对象,互不影响

  • 使用 ThreadLocal 生成线程的本地对象副本

3. 采用合适的方式处理竞争资源

多线程任务中,涉及到资源竞争的部分,通常都需要采用对应的措施来保证资源的一致性。常见的解决方案有两种:

  • 对资源加线程锁

  • 采用乐观策略实现无锁操作(CAS)

线程锁的使用:

使用线程锁来保证资源的一致性是由来已久的一种非常简单便捷的方法。这种操作可以粗暴的控制多个线程对资源的访问,所以在处理多线程资源竞争关系的时候,我们通常会优先想到加锁的方式。

为了提高执行性能,通常会采用轻量级锁来代替重量级锁,在 Java 1.5 中 synchronize 是一个重量级锁,是相对低效率的;相比之下使用 Lock 对象的性能更高一些。但是这种情况到了 Java 1.6 发生了很大的变化,由于官方对 synchronize 引入了适应自旋、锁消除、轻量级锁、偏向锁等优化手段, synchronize 与 Lock 在性能上不存在什么差距。所以如果你使用高于 Java 1.6 的版本,请放心大胆的使用 synchronize 。

无锁操作(CAS):

对于传统的加锁操作,我们通常认为是悲观策略。相对于悲观策略,我们还有一个乐观策略可以选择。乐观策略认为不会存在资源不一致的情况,假如出现了,就再试一次。

实际上在 Java 中,一些锁的实现也利用了 CAS,体现在 Java 中的应用如下:

应用领域

示例

java.util.concurrent.atomic 包

AtomicInteger
AtomicLong
AtomicBoolean
AtomicReference
AtomicStampedReference

java.util.concurrent 包

ConcurrentHashMap
ConcurrentLinkedQueue
ConcurrentLinkedDeque

java.util.concurrent.locks 包

ReentrantLock
ReentrantReadWriteLock

Java 内存模型

volatile 关键字的应用
synchronized 关键字的实现

4. 减少数据拷贝次数

这里的数据拷贝,更多指的是虚拟机或者操作系统层面的数据拷贝。我们都知道,在程序的运行过程中,通常会涉及到 IO 操作,不管是磁盘 IO 、内存 IO、网络 IO、标准 IO 等。所以基于 IO 的优化,通常也是一个非常有效的方向。

具体的优化手段有:

  • 尽量避免磁盘 IO,磁盘 IO 受限于磁盘的读写速度,相对较慢,尽量避免

  • 批量处理数组,尽量使用 System.arraycopy() 这样的批量接口,而不是通过遍历的方式自行操作

  • 启用大页内存,操作系统默认页大小是 4K,如果你的 heap 是 4G,就需要执行 1024*1024 次分配操作。 所以最好根据实际情况调整页大小。具体的调整方式需要从 JVM 和 操作系统两方面着手。JVM 中可以通过 -XX:+UseLargePages 进行配置,操作系统中修改比较麻烦,不在赘述,请自行解决。

5. 合适的 GC

所谓合适的 GC 主要体现在两方面

  • 针对不同的场景选择合适的 GC 策略

  • 针对 GC 配置合适的优化参数

GC 的选择

GC 类型

特点

适用场景

指定方式

Serial GC

单线程执行,Stop-the-World 暂停整个应用程序的执行来执行垃圾收集。适用于小型应用或客户端应用。

小型应用程序或客户端应用,对于资源受限的环境。

-XX:+UseSerialGC

Parallel GC

多线程执行,使用多个线程同时执行垃圾收集。Stop-the-World 暂停较短。适用于需要更高吞吐量的应用程序。

高吞吐量的服务器应用程序,如 Web 服务器或后端应用程序。

-XX:+UseParallelGC

CMS (Concurrent Mark-Sweep) GC

采用并发方式执行大部分垃圾收集工作,减少 Stop-the-World 时间。但会引入额外的 CPU 和内存开销。适用于需要更低的停顿时间的应用程序。

需要更低停顿时间的应用程序,如对延迟敏感的 Web 应用或交互式应用。

-XX:+UseConcMarkSweepGC

G1 (Garbage-First) GC

将堆分成多个区域,通过优先收集垃圾数量最多的区域来最大程度地减少停顿时间。适用于大堆,需要更可控和稳定的停顿时间的应用程序。

大型应用程序,需要更稳定和可预测的 GC 行为,如需要更可控的停顿时间,同时不愿意牺牲太多吞吐量的应用程序。

-XX:+UseG1GC

GC 的优化参数

参数

作用

使用前提

使用场景

-XX:MaxGCPauseMillis=<value>

设置期望的最大垃圾收集停顿时间(毫秒),用于调整收集器的目标停顿时间。

使用并行垃圾收集器(Parallel GC 或 G1 GC)时生效。

需要控制垃圾收集停顿时间的应用程序。

-XX:G1HeapRegionSize=<value>

设置 G1 堆区域的大小,影响 G1 收集器的工作表现。

使用 G1 垃圾收集器时生效。

需要调整 G1 收集器的性能和行为的应用程序。

-XX:ParallelGCThreads=<value>

设置并行垃圾收集器的线程数量,用于调整并行收集的线程数。

使用并行垃圾收集器(Parallel GC 或 Parallel Old GC)时生效。

需要调整并行垃圾收集器线程数的应用程序。

-XX:ConcGCThreads=<value>

设置 CMS 垃圾收集器的并发线程数,影响 CMS 收集器的并发能力。

使用 CMS 垃圾收集器时生效。

需要调整 CMS 垃圾收集器并发线程数的应用程序。

-XX:G1NewSizePercent=<value>

设置新生代空间占整个堆空间的百分比,影响 G1 收集器的新生代大小。

使用 G1 垃圾收集器时生效。

需要调整 G1 收集器新生代大小的应用程序。

-XX:G1MaxNewSizePercent=

设置最大新生代空间占整个堆空间的百分比,影响 G1 收集器的新生代最大大小。

使用 G1 垃圾收集器时生效。

需要调整 G1 收集器新生代最大大小的应用程序。

-XX:InitiatingHeapOccupancyPercent=<value>

设置 G1 收集器开始执行混合收集的堆占用百分比。

使用 G1 垃圾收集器时生效。

需要调整 G1 收集器开始执行混合收集的堆占用百分比的应用程序。

-XX:CMSInitiatingOccupancyFraction=<value>

设置 CMS 收集器开始执行垃圾收集的堆占用百分比。

使用 CMS 垃圾收集器时生效。

需要调整 CMS 收集器开始执行垃圾收集的堆占用百分比的应用程序。

-XX:MaxTenuringThreshold=<value>

设置对象晋升到老年代的年龄阈值,影响对象在新生代和老年代之间的迁移行为。

通常在需要调整对象晋升行为时使用,如调整新生代对象晋升到老年代的年龄阈值。

需要调整对象晋升行为的应用程序,如减少对象晋升到老年代的频率以减少老年代垃圾收集的负载。

-XX:+PrintGCDetails

打印 GC 详细信息,包括 GC 类型、停顿时间、堆空间使用情况等。

无特定前提。

监控和调试 GC 行为的应用程序。

配置样例:高吞吐 GC 配置

对于高吞吐量,在年轻态可以使用 Parallel Scavenge,年老态可以使用 Parallel Old 垃圾收集器。

  • 使用 -XX:+UseParallelOldGC 开启

  • 可以将 -XX:ParallelGCThreads 根据 CPU 的个数进行调整。可以是 CPU 数的 1/2 或者 5/8

配置样例:低延迟 GC 配置

对于低延迟的应用,在年轻态可以使用 ParNew,年老态可以使用 CMS 垃圾收集器。

  • 可以使用 -XX:+UseConcMarkSweepGC 和 -XX:+UseParNewGC 打开。

  • 可以将 -XX:ParallelGCThreads 根据 CPU 的个数进行调整。可以是 CPU 数的 1/2 或者 5/8

  • 可以调整 -XX:MaxTenuringThreshold (晋升年老代年龄) 调高,默认是 15. 这样可以减少年老代 GC 的压力

  • 可以 -XX:TargetSurvivorRatio,调整 Survivor 的占用比率。默认 50%. 调高可以提供 Survivor 区的利用率

  • 可以调整 -XX:SurvivorRatio,调整 Eden 和 Survivor 的比重。默认是 8. 这个比重越小,Survivor 越大,对象可以在年轻态呆更多时间。

如何验证一个 JSON 是否合法
如何验证一个 JSON 是否合法
在数据采集中使用对象池的实践
在数据采集中使用对象池的实践
在业务中使用 Kafka 到底能不能保证消息的有序性
数据处理中的责任链模式
数据处理中的责任链模式
探索 Kafka 消息丢失的问题和解决方案
SpringBoot 中实现订单过期自动取消
SpringBoot 中实现订单过期自动取消