在現代電腦系統中,可以有多個CPU,每個CPU又可以有多核心。為了充分利用現代CPU的功能,JAVA中引入了多線程,不同的線程可以同時在不同CPU或不同CPU核中運行。但對於JAVA程式猿來說要創建多少執行緒是可以自己控制的,但是執行緒到底運行在哪個CPU上,則是一個黑盒子,一般來說很難得知。
但是如果是不同CPU核對相同執行緒進行調度,則可能會出現CPU切換造成的效能損失。一般情況下這種損失是比較小的,但是如果你的程式特別在意這種CPU切換帶來的損耗,那麼可以試試今天要講的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策略可以有順序,在前面的策略會先匹配,如果匹配不上則會選擇第二策略,依此類推。
接下來我們看下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,和之前的分析相匹配。
上面我们提到的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中將執行緒綁定到特定的CPU?的詳細內容。更多資訊請關注PHP中文網其他相關文章!