日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪(fǎng)問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

多线程中的volatile和伪共享

發(fā)布時(shí)間:2023/12/1 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 多线程中的volatile和伪共享 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

? 偽共享 false sharing,顧名思義,“偽共享”就是“其實(shí)不是共享”。那什么是“共享”?多CPU同時(shí)訪(fǎng)問(wèn)同一塊內(nèi)存區(qū)域就是“共享”,就會(huì)產(chǎn)生沖突,需要控制協(xié)議來(lái)協(xié)調(diào)訪(fǎng)問(wèn)。會(huì)引起“共享”的最小內(nèi)存區(qū)域大小就是一個(gè)cache line。因此,當(dāng)兩個(gè)以上CPU都要訪(fǎng)問(wèn)同一個(gè)cache line大小的內(nèi)存區(qū)域時(shí),就會(huì)引起沖突,這種情況就叫“共享”。但是,這種情況里面又包含了“其實(shí)不是共享”的“偽共享”情況。比如,兩個(gè)處理器各要訪(fǎng)問(wèn)一個(gè)word,這兩個(gè)word卻存在于同一個(gè)cache line大小的區(qū)域里,這時(shí),從應(yīng)用邏輯層面說(shuō),這兩個(gè)處理器并沒(méi)有共享內(nèi)存,因?yàn)樗麄冊(cè)L問(wèn)的是不同的內(nèi)容(不同的word)。但是因?yàn)閏ache line的存在和限制,這兩個(gè)CPU要訪(fǎng)問(wèn)這兩個(gè)不同的word時(shí),卻一定要訪(fǎng)問(wèn)同一個(gè)cache line塊,產(chǎn)生了事實(shí)上的“共享”。顯然,由于cache line大小限制帶來(lái)的這種“偽共享”是我們不想要的,會(huì)浪費(fèi)系統(tǒng)資源。

  緩存系統(tǒng)中是以緩存行(cache line)為單位存儲(chǔ)的。緩存行是2的整數(shù)冪個(gè)連續(xù)字節(jié),一般為32-256個(gè)字節(jié)。最常見(jiàn)的緩存行大小是64個(gè)字節(jié)。當(dāng)多線(xiàn)程修改互相獨(dú)立的變量時(shí),如果這些變量共享同一個(gè)緩存行,就會(huì)無(wú)意中影響彼此的性能,這就是偽共享。緩存行上的寫(xiě)競(jìng)爭(zhēng)是運(yùn)行在SMP系統(tǒng)中并行線(xiàn)程實(shí)現(xiàn)可伸縮性最重要的限制因素。有人將偽共享描述成無(wú)聲的性能殺手,因?yàn)閺拇a中很難看清楚是否會(huì)出現(xiàn)偽共享。

  為了讓可伸縮性與線(xiàn)程數(shù)呈線(xiàn)性關(guān)系,就必須確保不會(huì)有兩個(gè)線(xiàn)程往同一個(gè)變量或緩存行中寫(xiě)。兩個(gè)線(xiàn)程寫(xiě)同一個(gè)變量可以在代碼中發(fā)現(xiàn)。為了確定互相獨(dú)立的變量是否共享了同一個(gè)緩存行,就需要了解內(nèi)存布局,或找個(gè)工具告訴我們。Intel VTune就是這樣一個(gè)分析工具。

  圖1說(shuō)明了偽共享的問(wèn)題。在核心1上運(yùn)行的線(xiàn)程想更新變量X,同時(shí)核心2上的線(xiàn)程想要更新變量Y。不幸的是,這兩個(gè)變量在同一個(gè)緩存行中。每個(gè)線(xiàn)程都要去競(jìng)爭(zhēng)緩存行的所有權(quán)來(lái)更新變量。如果核心1獲得了所有權(quán),緩存子系統(tǒng)將會(huì)使核心2中對(duì)應(yīng)的緩存行失效。當(dāng)核心2獲得了所有權(quán)然后執(zhí)行更新操作,核心1就要使自己對(duì)應(yīng)的緩存行失效。這會(huì)來(lái)來(lái)回回的經(jīng)過(guò)L3緩存,大大影響了性能。如果互相競(jìng)爭(zhēng)的核心位于不同的插槽,就要額外橫跨插槽連接,問(wèn)題可能更加嚴(yán)重。

  Java Memory Layout Java內(nèi)存布局,在項(xiàng)目開(kāi)發(fā)中,大多使用HotSpot的JVM,hotspot中對(duì)象都有兩個(gè)字(四字節(jié))長(zhǎng)的對(duì)象頭。第一個(gè)字是由24位哈希碼和8位標(biāo)志位(如鎖的狀態(tài)或作為鎖對(duì)象)組成的Mark Word。第二個(gè)字是對(duì)象所屬類(lèi)的引用。如果是數(shù)組對(duì)象還需要一個(gè)額外的字來(lái)存儲(chǔ)數(shù)組的長(zhǎng)度。每個(gè)對(duì)象的起始地址都對(duì)齊于8字節(jié)以提高性能。因此當(dāng)封裝對(duì)象的時(shí)候?yàn)榱烁咝?#xff0c;對(duì)象字段聲明的順序會(huì)被重排序成下列基于字節(jié)大小的順序:

?

  • double (8字節(jié)) 和 long (8字節(jié))
  • int (4字節(jié)) 和 float (4字節(jié))
  • short (2字節(jié)) 和 char (2字節(jié)):char在java中是2個(gè)字節(jié)。java采用unicode,2個(gè)字節(jié)(16位)來(lái)表示一個(gè)字符。
  • boolean (1字節(jié)) 和 byte (1字節(jié))
  • reference引用 (4/8 字節(jié))
  • <子類(lèi)字段重復(fù)上述順序>

?

在了解這些之后,就可以在任意字段間用7個(gè)long來(lái)填充緩存行。偽共享在不同的JDK下提供了不同的解決方案。

  在JDK1.6環(huán)境下,解決偽共享的辦法是使用緩存行填充,使一個(gè)對(duì)象占用的內(nèi)存大小剛好為64bytes或它的整數(shù)倍,這樣就保證了一個(gè)緩存行里不會(huì)有多個(gè)對(duì)象。

?

package basic;public class TestFlash implements Runnable {public final static int NUM_THREADS = 4; // changepublic final static long ITERATIONS = 500L * 1000L * 1000L;private final int arrayIndex;/*** 為了展示其性能影響,我們啟動(dòng)幾個(gè)線(xiàn)程,每個(gè)都更新它自己獨(dú)立的計(jì)數(shù)器。計(jì)數(shù)器是volatile long類(lèi)型的,所以其它線(xiàn)程能看到它們的進(jìn)展。*/public final static class VolatileLong {/* 用volatile[?vɑ:l?tl]修飾的變量,線(xiàn)程在每次使用變量的時(shí)候,JVM虛擬機(jī)只保證從主內(nèi)存加載到線(xiàn)程工作內(nèi)存的值是最新的 */public volatile long value = 0L;/* 緩沖行填充 *//* 37370571461 :不使用緩沖行執(zhí)行納秒數(shù) *//* 16174480826 :使用緩沖行執(zhí)行納秒數(shù),性能提高一半 */public long p1, p2, p3, p4, p5, p6, p7;}private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];static {for (int i = 0; i < longs.length; i++) {longs[i] = new VolatileLong();}}public TestFlash(final int arrayIndex){this.arrayIndex = arrayIndex;}/*** 我們不能確定這些VolatileLong會(huì)布局在內(nèi)存的什么位置。它們是獨(dú)立的對(duì)象。但是經(jīng)驗(yàn)告訴我們同一時(shí)間分配的對(duì)象趨向集中于一塊。*/public static void main(final String[] args) throws Exception {final long start = System.nanoTime();runTest();System.out.println("duration = " + (System.nanoTime() - start));}private static void runTest() throws InterruptedException {Thread[] threads = new Thread[NUM_THREADS];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new TestFlash(i));}for (Thread t : threads) {t.start();}for (Thread t : threads) {t.join();}}/** 為了展示其性能影響,我們啟動(dòng)幾個(gè)線(xiàn)程,每個(gè)都更新它自己獨(dú)立的計(jì)數(shù)器。計(jì)數(shù)器是volatile long類(lèi)型的,所以其它線(xiàn)程能看到它們的進(jìn)展*/@Overridepublic void run() {long i = ITERATIONS + 1;while (0 != --i) {longs[arrayIndex].value = i;}} }

?

VolatileLong通過(guò)填充一些無(wú)用的字段p1,p2,p3,p4,p5,p6,再考慮到對(duì)象頭也占用8bit, 剛好把對(duì)象占用的內(nèi)存擴(kuò)展到剛好占64bytes(或者64bytes的整數(shù)倍)。這樣就避免了一個(gè)緩存行中加載多個(gè)對(duì)象。但這個(gè)方法現(xiàn)在只能適應(yīng)JAVA6 及以前的版本了。

  在jdk1.7環(huán)境下,由于java 7會(huì)優(yōu)化掉無(wú)用的字段。因此,JAVA 7下做緩存行填充更麻煩了,需要使用繼承的辦法來(lái)避免填充被優(yōu)化掉。把填充放在基類(lèi)里面,可以避免優(yōu)化(這好像沒(méi)有什么道理好講的,JAVA7的內(nèi)存優(yōu)化算法問(wèn)題,能繞則繞)。

?

package basic;public class TestFlashONJDK7 implements Runnable {public static int NUM_THREADS = 4;public final static long ITERATIONS = 500L * 1000L * 1000L;private final int arrayIndex;private static VolatileLong[] longs;public TestFlashONJDK7(final int arrayIndex){this.arrayIndex = arrayIndex;}public static void main(final String[] args) throws Exception {Thread.sleep(10000);System.out.println("starting....");if (args.length == 1) {NUM_THREADS = Integer.parseInt(args[0]);}longs = new VolatileLong[NUM_THREADS];for (int i = 0; i < longs.length; i++) {longs[i] = new VolatileLong();}final long start = System.nanoTime();runTest();System.out.println("duration = " + (System.nanoTime() - start));}private static void runTest() throws InterruptedException {Thread[] threads = new Thread[NUM_THREADS];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new TestFlashONJDK7(i));}for (Thread t : threads) {t.start();}for (Thread t : threads) {t.join();}}@Overridepublic void run() {long i = ITERATIONS + 1;while (0 != --i) {longs[arrayIndex].value = i;}} }class VolatileLong extends VolatileLongPadding {public volatile long value = 0L; }class VolatileLongPadding {public volatile long p1, p2, p3, p4, p5, p6, p7; }

?

在jdk1.8環(huán)境下,緩存行填充終于被JAVA原生支持了。JAVA 8中添加了一個(gè)@Contended的注解,添加這個(gè)的注解,將會(huì)在自動(dòng)進(jìn)行緩存行填充。以上的例子可以改為:

?

package basic;public class TestFlashONJDK8 implements Runnable {public static int NUM_THREADS = 4;public final static long ITERATIONS = 500L * 1000L * 1000L;private final int arrayIndex;private static VolatileLong[] longs;public TestFlashONJDK8(final int arrayIndex){this.arrayIndex = arrayIndex;}public static void main(final String[] args) throws Exception {Thread.sleep(10000);System.out.println("starting....");if (args.length == 1) {NUM_THREADS = Integer.parseInt(args[0]);}longs = new VolatileLong[NUM_THREADS];for (int i = 0; i < longs.length; i++) {longs[i] = new VolatileLong();}final long start = System.nanoTime();runTest();System.out.println("duration = " + (System.nanoTime() - start));}private static void runTest() throws InterruptedException {Thread[] threads = new Thread[NUM_THREADS];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new TestFlashONJDK8(i));}for (Thread t : threads) {t.start();}for (Thread t : threads) {t.join();}}@Overridepublic void run() {long i = ITERATIONS + 1;while (0 != --i) {longs[arrayIndex].value = i;}} } @Contended class VolatileLong {

  public volatile long value = 0L;
}

?

?

執(zhí)行時(shí),必須加上虛擬機(jī)參數(shù)-XX:-RestrictContended,@Contended注釋才會(huì)生效。很多文章把這個(gè)漏掉了,那樣的話(huà)實(shí)際上就沒(méi)有起作用。

?

補(bǔ)充:

byte字節(jié) ?bit位 1byte=8bit

volatile說(shuō)明

package basic;public class TestVolatile {public static int count = 0;/* 即使使用volatile,依舊沒(méi)有達(dá)到我們期望的效果 */// public volatile static int count = 0;public static void increase() {try {// 延遲10毫秒,使得結(jié)果明顯Thread.sleep(10);count++;} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {for (int i = 0; i < 10000; i++) {new Thread(new Runnable() {@Overridepublic void run() {TestVolatile.increase();}}).start();}System.out.println("期望運(yùn)行結(jié)果:10000");System.out.println("實(shí)際運(yùn)行結(jié)果:" + TestVolatile.count);} }

volatile關(guān)鍵字的使用:用volatile修飾的變量,線(xiàn)程在每次使用變量的時(shí)候,都會(huì)讀取變量修改后的最新值。但是由于操作不是原子性的,對(duì)于volatile修飾的變量,jvm虛擬機(jī)只是保證從主內(nèi)存加載到線(xiàn)程工作內(nèi)存的值是最新的。

在java 垃圾回收整理一文中,描述了jvm運(yùn)行時(shí)刻內(nèi)存的分配。其中有一個(gè)內(nèi)存區(qū)域是jvm虛擬機(jī)棧,每一個(gè)線(xiàn)程運(yùn)行時(shí)都有一個(gè)線(xiàn)程棧,線(xiàn)程棧保存了線(xiàn)程運(yùn)行時(shí)候變量值信息。當(dāng)線(xiàn)程訪(fǎng)問(wèn)某一個(gè)對(duì)象時(shí)候值的時(shí)候,首先通過(guò)對(duì)象的引用找到對(duì)應(yīng)在堆內(nèi)存的變量的值,然后把堆內(nèi)存變量的具體值load到線(xiàn)程本地內(nèi)存中,建立一個(gè)變量副本,之后線(xiàn)程就不再和對(duì)象在堆內(nèi)存變量值有任何關(guān)系,而是直接修改副本變量的值,在修改完之后的某一個(gè)時(shí)刻(線(xiàn)程退出之前),自動(dòng)把線(xiàn)程變量副本的值回寫(xiě)到對(duì)象在堆中變量。這樣在堆中的對(duì)象的值就產(chǎn)生變化了。上面一幅圖描述這些交互,過(guò)程如下:

?

  • read and load 從主存復(fù)制變量到當(dāng)前工作內(nèi)存
  • use and assign? 執(zhí)行代碼,改變共享變量值(其中use and assign 可以多次出現(xiàn))?
  • store and write 用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容

但是這些操作并不是原子性,也就是在read load之后,如果主內(nèi)存count變量發(fā)生修改之后,線(xiàn)程工作內(nèi)存中的值由于已經(jīng)加載,不會(huì)產(chǎn)生對(duì)應(yīng)的變化,所以計(jì)算出來(lái)的結(jié)果會(huì)和預(yù)期不一樣。對(duì)于volatile修飾的變量,JVM虛擬機(jī)只是保證從主內(nèi)存加載到線(xiàn)程工作內(nèi)存的值是最新的。例如假如線(xiàn)程1,線(xiàn)程2在進(jìn)行read load操作中,發(fā)現(xiàn)主內(nèi)存中count的值都是5,那么都會(huì)加載這個(gè)最新的值。在線(xiàn)程1堆count進(jìn)行修改之后,會(huì)write到主內(nèi)存中,主內(nèi)存中的count變量就會(huì)變?yōu)?。線(xiàn)程2由于已經(jīng)進(jìn)行read,load操作,在進(jìn)行運(yùn)算之后,也會(huì)更新主內(nèi)存count的變量值為6。導(dǎo)致兩個(gè)線(xiàn)程即使使用volatile關(guān)鍵字修改之后,還是會(huì)存在并發(fā)的情況。

對(duì)于volatile修飾的變量,JVM虛擬機(jī)只能保證從主內(nèi)存加載到線(xiàn)程工作內(nèi)存的值是最新的。

參考博客:

[1] http://www.cnblogs.com/Binhua-Liu/p/5620339.html

?

轉(zhuǎn)載于:https://www.cnblogs.com/RunForLove/p/5624390.html

總結(jié)

以上是生活随笔為你收集整理的多线程中的volatile和伪共享的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。