当前位置: 首页 > 后端技术 > Java

java进阶用法:CPU绑定线程Thread-Affinity

时间:2023-04-01 19:44:33 Java

介绍现代计算机系统中,可以有多个CPU,每个CPU可以有多个核心。为了充分利用现代CPU的功能,在JAVA中引入了多线程,不同的线程可以同时运行在不同的CPU或者不同的CPU核上。但是对于JAVA程序员来说,创建多少个线程是可以自己控制的,但是线程运行在哪个CPU上是一个黑盒子,一般很难知道。但是,如果不同的CPU核心调度同一个线程,可能会出现CPU切换带来的性能损失。一般情况下,这个损失比较小,但是如果你的程序特别在意这种CPU切换带来的损失,可以试试今天要讲的JavaThreadAffinity。Java线程亲和性简介JAVA代码中的线程与CPU特定的核心绑定,以提高程序性能。显然,java线程Affinity为了与底层CPU进行交互,必须使用JAVA与native方法进行交互的方式。JNI虽然是JAVA和JAVA原生方法交互的官方方法,但是JNI使用起来比较麻烦。.所以javathreadAffinity其实使用的是JNA,它是在JNI基础上改进的一个库,用来和native方法进行交互。首先介绍一下CPU中的几个概念,即CPU、CPUsocket和CPUcore。首先是中央处理器。CPU的全称是CentralProcessingUnit,又称中央处理器,是任务处理的关键核心。那么什么是CPU插槽呢?所谓插座就是插CPU的插座。如果你组装过台式电脑,你应该知道CPU是安装在插座上的。CPUCore是指CPU中的核心数。很久以前,CPU是单核的,但是随着多核技术的发展,一个CPU可以包含多个核,CPU中的核才是真正的业务处理。单元。如果是linux机器,可以使用lscpu命令查看系统的CPU状态,如下:Architecture:x86_64CPUop-mode(s):32-bit,64-bitByteOrder:LittleEndianCPU(s):1在线CPU列表:0每个内核线程:每个插槽1个内核:1个插槽:1个NUMA节点:1个供应商ID:GenuineIntelCPU系列:6型号:94型号名称:Intel(R)Xeon(R)Gold6148CPU@2.40GHzStepping:3CPUMHz:2400.000BogoMIPS:4800.00Hypervisorvendor:KVMVirtualizationtype:fullL1dcache:32KL1icache:32KL2cache:4096KL3cache:28160KNUMAnode0CPU(s):0Flags:fpuvmecdepsemsrpaemcecx8apicsepmtrrpgemcacmovpatpse36clflushmmxfxsrssesse2ss系统调用nxpdpe1gbrdtscplmconstant_tscrep_goodnopleagerfpupnibepclmulqdqssse3fmacx16pcidsse4_1sse4_2movmovpopcnttsc_deadline_timeraesxsaveavxf16crdrandhypervisorlahf_lmabm3dnowprefetchinvpcid_singlefsgsbasebmi1hleavx2smepbmi2ermsinvpcidrtmmpxavx512favx512dqrdseedadxsmapavx512cdavx512bwavx512vlxsaveoptxsavecxgetbv1arat从上面的输出我们可以看到,这个服务器有一个socket,每每个socket都有一个核心,每个核心可以同时处理1个线程。这些CPU的信息可以称为CPU布局。在Linux中,CPU的布局信息存放在/proc/cpuinfo中。JavaThreadAffinity中有一个CpuLayout接口对应这个信息:publicinterfaceCpuLayout{intcpus();国际套接字();intcoresPerSocket();intthreadsPerCore();intsocketId(intcpuId);intcoreId(intcpuId);intthreadId(intcpuId);}AffinityStrategies根据CPU布局的信息,提供了一些基本的Affinity策略来安排不同线程之间的分布关系,主要如下:SAME_CORE——运行在同一个核心上。SAME_SOCKET-在同一个套接字上运行,但不在同一个内核上。DIFFERENT_SOCKET-在不同的套接字上运行DIFFERENT_CORE-在不同的内核上运行ANY-任何情况都可以。这些策略也是根据CpuLayout的socketId和coreId来区分的。下面以SAME_CORE为例,按其具体实现:SAME_CORE{@Overridepublicbooleanmatches(intcpuId,intcpuId2){返回cpuLayout.socketId(cpuId)==cpuLayout.socketId(cpuId2)&&cpuLayout.coreId(cpuId)==cpuLayout.coreId(cpuId2);}}亲和策略是可以排序的,先匹配上一个策略,不匹配则选择第二个策略,以此类推。AffinityLock的使用接下来我们看一下Affinity的具体使用。首先是获得CPU锁。在JAVA7之前,我们可以这样写:AffinityLockal=AffinityLock.acquireLock();try{//dosomeworklockedtoaCPU.}finally{al.release();}在JAVA7之后,你可以这样写:try(AffinityLockal=AffinityLock.acquireLock()){//在锁定时做一些工作到CPU。}acquireLock方法可以获得任何可用的cpu。这是一个粗粒度的锁。如果你想获得一个细粒度的核心,你可以使用acquireCore:try(AffinityLockal=AffinityLock.acquireCore()){//dosomeworkwhilelockedtoaCPU.}acquireLock还有一个bind参数,表示是否当前线程绑定到获得的cpu锁上,如果bind参数=true,那么当前线程将运行在acquireLock中获得的CPU上。如果bind参数=false,表示acquireLock会在以后的某个时间点绑定。上面我们提到了AffinityStrategy,这个AffinityStrategy可以作为acquireLock的参数:通过调用当前AffinityLock的acquireLock方法,当前线程可以Allocate一个与之前的锁策略相关的AffinityLock。AffinityLock还提供了一个dumpLocks方法来查看当前CPU和线程的绑定状态。让我们举个例子:privatestaticfinalExecutorServiceES=Executors.newFixedThreadPool(4,newAffinityThreadFactory("bg",SAME_CORE,DIFFERENT_SOCKET,ANY));对于(inti=0;i<12;i++)ES.submit(newCallable(){@OverridepublicVoidcall()throwsInterruptedException{Thread.sleep(100);returnnull;}});线程.睡眠(200);System.out.println("\nCPU的分配为\n"+AffinityLock.dumpLocks());ES.关机();ES.awaitTermination(1,TimeUnit.SECONDS);上面代码中,我们创建了一个线程池,有4个线程,对应的ThreadFactory为AffinityThreadFactory,将线程池命名为bg,并分配了3个AffinityStrategy。这意味着它首先分配给同一个核心,然后分配给不同的插槽,最后分配给任何可用的CPU。然后在具体的执行过程中,我们提交了12个线程,但是我们的线程池最多只有4个线程。可以预见,AffinityLock.dumpLocks方法返回的结果中只有4个线程会绑定CPU。我们来看一下:CPU的分配是0:CPU不可用1:为这个应用保留2:为这个应用保留3:为这个应用保留4:Thread[bg-4,5,main]alive=true5:Thread[bg-3,5,main]alive=true6:Thread[bg-2,5,main]alive=true7:Thread[bg,5,main]alive=true从输出可以看出,CPU0不可用。其他7个CPU可用,但只绑定到4个线程,这与我们之前的分析相符。接下来,我们修改AffinityThreadFactory的AffinityStrategy,如下:newAffinityThreadFactory("bg",SAME_CORE)表示线程只会绑定同一个core,因为在目前的硬件中,一个core只能支持一个Thread绑定,所以它可以预见最终结果只会绑定一个线程,运行结果如下::Reservedforthisapplication6:Reservedforthisapplication7:Thread[bg,5,main]alive=true可以看到只有第一个线程绑定了CPU,与前面的分析相符。使用API??直接分配CPU。上面提到的AffinityLock的acquireLock方法其实可以接受一个CPUid参数,直接用于获取传入CPUid的锁。这样后续线程就可以在指定的CPU上运行。publicstaticAffinityLockacquireLock(intcpuId){returnacquireLock(true,cpuId,AffinityStrategies.ANY);实时的,这种Affinity存储在BitSet中,BitSet的索引就是CPU的id,对应的值就是是否获取锁。先看setAffinity方法的定义:publicstaticvoidsetAffinity(intcpu){BitSetaffinity=newBitSet(Runtime.getRuntime().availableProcessors());亲和力。设置(CPU);设置亲和力(亲和力);setAffinity的使用:longcurrentAffinity=AffinitySupport.getAffinity();Affinity.setAffinity(1L<<5);//锁定到CPU5。注意,由于BitSet底层使用Long进行数据存储,所以这里的索引是位索引,所以我们需要将CPU索引转换成十进制。总结JavaThreadAffinity可以从JAVA代码中控制程序中Thread使用的CPU。它非常强大,每个人都可以使用它。本文已收录于http://www.flydean.com/01-java-thread-affinity/最流行的解读,最深刻的干货,最简洁的教程,很多你不知道的小技巧等着你等你发现!欢迎关注我的公众号:《程序那些事儿》,懂技术,更懂你!