java高级用法之:绑定CPU的线程Thread-Affinity

简介: 在现代计算机系统中,可以有多个CPU,每个CPU又可以有多核。为了充分利用现代CPU的功能,JAVA中引入了多线程,不同的线程可以同时在不同CPU或者不同CPU核中运行。但是对于JAVA程序猿来说创建多少线程是可以自己控制的,但是线程到底运行在哪个CPU上,则是一个黑盒子,一般来说很难得知。

简介

在现代计算机系统中,可以有多个CPU,每个CPU又可以有多核。为了充分利用现代CPU的功能,JAVA中引入了多线程,不同的线程可以同时在不同CPU或者不同CPU核中运行。但是对于JAVA程序猿来说创建多少线程是可以自己控制的,但是线程到底运行在哪个CPU上,则是一个黑盒子,一般来说很难得知。

但是如果是不同CPU核对同一线程进行调度,则可能会出现CPU切换造成的性能损失。一般情况下这种损失是比较小的,但是如果你的程序特别在意这种CPU切换带来的损耗,那么可以试试今天要讲的Java Thread Affinity.

Java Thread Affinity简介

java thread Affinity是用来将JAVA代码中的线程绑定到CPU特定的核上,用来提升程序运行的性能。

很显然,要想和底层的CPU进行交互,java thread Affinity一定会用到JAVA和native方法进行交互的方法,JNI虽然是JAVA官方的JAVA和native方法进行交互的方法,但是JNI在使用起来比较繁琐。所以java thread Affinity实际使用的是JNA,JNA是在JNI的基础上进行改良的一种和native方法进行交互的库。

先来介绍CPU中几个概念,分别是CPU,CPU socket和CPU core。

首先是CPU,CPU的全称就是central processing unit,又叫做中央处理器,就是用来进行任务处理的关键核心。

那么什么是CPU socket呢?所谓socket就是插CPU的插槽,如果组装过台式机的同学应该都知道,CPU就是安装在Socket上的。

CPU Core指的是CPU中的核数,在很久之前CPU都是单核的,但是随着多核技术的发展,一个CPU中可以包含多个核,而CPU中的核就是真正的进行业务处理的单元。

如果你是在linux机子上,那么可以通过使用lscpu命令来查看系统的CPU情况,如下所示:

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                1
On-line CPU(s) list:   0
Thread(s) per core:    1
Core(s) per socket:    1
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 94
Model name:            Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz
Stepping:              3
CPU MHz:               2400.000
BogoMIPS:              4800.00
Hypervisor vendor:     KVM
Virtualization type:   full
L1d cache:             32K
L1i cache:             32K
L2 cache:              4096K
L3 cache:              28160K
NUMA node0 CPU(s):     0
Flags:                 fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single fsgsbase bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx smap avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 arat

从上面的输出我们可以看到,这个服务器有一个socket,每个socket有一个core,每个core可以同时处理1个线程。

这些CPU的信息可以称为CPU layout。在linux中CPU的layout信息是存放在/proc/cpuinfo中的。

在Java Thread Affinity中有一个CpuLayout接口用来和这些信息进行对应:

public interface CpuLayout {
    int cpus();
    int sockets();
    int coresPerSocket();
    int threadsPerCore();
    int socketId(int cpuId);
    int coreId(int cpuId);
    int threadId(int cpuId);
}

根据CPU layout的信息, AffinityStrategies提供了一些基本的Affinity策略,用来安排不同的thread之间的分布关系,主要有下面几种:

SAME_CORE - 运行在同一个core中。
    SAME_SOCKET - 运行在同一个socket中,但是不在同一个core上。
    DIFFERENT_SOCKET - 运行在不同的socket中
    DIFFERENT_CORE - 运行在不同的core上
    ANY - 任何情况都可以

这些策略也都是根据CpuLayout的socketId和coreId来进行区分的,我们以SAME_CORE为例,按下它的具体实现:

SAME_CORE {
        @Override
        public boolean matches(int cpuId, int cpuId2) {
            CpuLayout cpuLayout = AffinityLock.cpuLayout();
            return cpuLayout.socketId(cpuId) == cpuLayout.socketId(cpuId2) &&
                    cpuLayout.coreId(cpuId) == cpuLayout.coreId(cpuId2);
        }
    }

Affinity策略可以有顺序,在前面的策略会首先匹配,如果匹配不上则会选择第二策略,依此类推。

AffinityLock的使用

接下来我们看下Affinity的具体使用,首先是获得一个CPU的lock,在JAVA7之前,我们可以这样写:

AffinityLock al = AffinityLock.acquireLock();
try {
     // do some work locked to a CPU.
} finally {
     al.release();
}

在JAVA7之后,可以这样写:

try (AffinityLock al = AffinityLock.acquireLock()) {
    // do some work while locked to a CPU.
}

acquireLock方法可以为线程获得任何可用的cpu。这个是一个粗粒度的lock。如果想要获得细粒度的core,可以用acquireCore:

try (AffinityLock al = AffinityLock.acquireCore()) {
    // do some work while locked to a CPU.
}

acquireLock还有一个bind参数,表示是否将当前的线程绑定到获得的cpu lock上,如果bind参数=true,那么当前的thread会在acquireLock中获得的CPU上运行。如果bind参数=false,表示acquireLock会在未来的某个时候进行bind。

上面我们提到了AffinityStrategy,这个AffinityStrategy可以作为acquireLock的参数使用:

public AffinityLock acquireLock(AffinityStrategy... strategies) {
        return acquireLock(false, cpuId, strategies);
    }

通过调用当前AffinityLock的acquireLock方法,可以为当前的线程分配和之前的lock策略相关的AffinityLock。

AffinityLock还提供了一个dumpLocks方法,用来查看当前CPU和thread的绑定状态。我们举个例子:

private static final ExecutorService ES = Executors.newFixedThreadPool(4,
           new AffinityThreadFactory("bg", SAME_CORE, DIFFERENT_SOCKET, ANY));
for (int i = 0; i < 12; i++)
            ES.submit(new Callable<Void>() {
                @Override
                public Void call() throws InterruptedException {
                    Thread.sleep(100);
                    return null;
                }
            });
        Thread.sleep(200);
        System.out.println("\nThe assignment of CPUs is\n" + AffinityLock.dumpLocks());
        ES.shutdown();
        ES.awaitTermination(1, TimeUnit.SECONDS);

上面的代码中,我们创建了一个4个线程的线程池,对应的ThreadFactory是AffinityThreadFactory,给线程池起名bg,并且分配了3个AffinityStrategy。 意思是首先分配到同一个core上,然后到不同的socket上,最后是任何可用的CPU。

然后具体执行的过程中,我们提交了12个线程,但是我们的Thread pool最多只有4个线程,可以预见, AffinityLock.dumpLocks方法返回的结果中只有4个线程会绑定CPU,一起来看看:

The assignment of CPUs is
0: CPU not available
1: Reserved for this application
2: Reserved for this application
3: Reserved for this application
4: Thread[bg-4,5,main] alive=true
5: Thread[bg-3,5,main] alive=true
6: Thread[bg-2,5,main] alive=true
7: Thread[bg,5,main] alive=true

从输出结果可以看到,CPU0是不可用的。其他7个CPU是可用的,但是只绑定了4个线程,这和我们之前的分析是匹配的。

接下来,我们把AffinityThreadFactory的AffinityStrategy修改一下,如下所示:

new AffinityThreadFactory("bg", SAME_CORE)

表示线程只会绑定到同一个core中,因为在当前的硬件中,一个core同时只能支持一个线程的绑定,所以可以预见最后的结果只会绑定一个线程,运行结果如下:

The assignment of CPUs is
0: CPU not available
1: Reserved for this application
2: Reserved for this application
3: Reserved for this application
4: Reserved for this application
5: Reserved for this application
6: Reserved for this application
7: Thread[bg,5,main] alive=true

可以看到只有第一个线程绑定了CPU,和之前的分析相匹配。

使用API直接分配CPU

上面我们提到的AffinityLock的acquireLock方法其实还可以接受一个CPU id参数,直接用来获得传入CPU id的lock。这样后续线程就可以在指定的CPU上运行。

public static AffinityLock acquireLock(int cpuId) {
        return acquireLock(true, cpuId, AffinityStrategies.ANY);
    }

实时上这种Affinity是存放在BitSet中的,BitSet的index就是cpu的id,对应的value就是是否获得锁。

先看下setAffinity方法的定义:

public static void setAffinity(int cpu) {
        BitSet affinity = new BitSet(Runtime.getRuntime().availableProcessors());
        affinity.set(cpu);
        setAffinity(affinity);
    }

再看下setAffinity的使用:

long currentAffinity = AffinitySupport.getAffinity();
Affinity.setAffinity(1L << 5); // lock to CPU 5.

注意,因为BitSet底层是用Long来进行数据存储的,所以这里的index是bit index,所以我们需要对十进制的CPU index进行转换。

总结

Java Thread Affinity可以从JAVA代码中对程序中Thread使用的CPU进行控制,非常强大,大家可以运用起来。

本文已收录于 http://www.flydean.com/01-java-thread-affinity/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

相关文章
|
3天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
40 17
|
14天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
15天前
|
移动开发 前端开发 Java
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
|
16天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
16天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
16天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
42 3
|
16天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
99 2
|
24天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
47 6
|
27天前
|
Java 对象存储 开发者
如何找出Java进程占用CPU高的元凶
本文记录了一次Java进程CPU占用率过高的问题和排查思路。
|
14天前
|
存储 缓存 前端开发
JavaEE初阶——初识EE(Java诞生背景,CPU详解)
带你从零入门JAVAEE初阶,Java的发展历程认识什么是cpu,cpu的工作原理,cpu是如何进行计算的,cpu的架构,指令集,cpu的核心,如何提升cpu的算力,cpu的指令,,cup的缓存,cpu的流水线