欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java并發(fā)程序刺客之假共享的原理及復現

 更新時間:2022年08月04日 14:55:34   作者:一無是處的研究僧  
前段時間在各種社交平臺“雪糕刺客”這個詞比較火,而在并發(fā)程序中也有一個刺客,那就是假共享。本文將通過示例詳細講解假共享的原理及復現,需要的可以參考一下

前言

前段時間在各種社交平臺“雪糕刺客”這個詞比較火,簡單的來說就是雪糕的價格非常高!其實在并發(fā)程序當中也有一個刺客,如果在寫并發(fā)程序的時候不注意不小心,這個刺客很可能會拖累我們的并發(fā)程序,讓我們并發(fā)程序執(zhí)行的效率變低,讓并發(fā)程序付出很大的代價,這和“雪糕刺客”當中的“刺客”的含義是一致的。這個并發(fā)程序當中的刺客就是——假共享(False Sharing)。

假共享(False Sharing)

緩存行

當CPU從更慢級別的緩存讀取數據的時候(三級Cache會從內存當中讀取數據,二級緩存會從三級緩存當中讀取數據,一級緩存會從二級緩存當中讀取數據,緩存級別越低執(zhí)行速度越快),CPU并不是一個字節(jié)一個字節(jié)的讀取的,而是一次會讀取一塊數據,然后將這個數據緩存到CPU當中,而這一塊數據就叫做緩存行。有一種緩存行的大小就是64字節(jié),那么我們?yōu)槭裁磿鲞@種優(yōu)化呢?這是因為局部性原理,所謂局部性原理簡單說來就是,當時使用一個數據的時候,它附近的數據在未來的一段時間你也很可能用到,比如說我們遍歷數組,我們通常從前往后進行遍歷,比如我們數組當中的數據大小是8個字節(jié),如果我們的緩存行是64個字節(jié)的話,那么一個緩存行就可以緩存8個數據,那么我們在遍歷第一個數據的時候將這8個數據加載進入緩存行,那么我們在遍歷未來7個數據的時候都不需要再從內存當中拿數據,直接從緩存當中拿就行,這就可以節(jié)約程序執(zhí)行的時間。

假共享

當兩個線程在CPU上兩個不同的核心上執(zhí)行代碼的時候,如果這兩個線程使用了同一個緩存行C,而且對這個緩存行當中兩個不同的變量進行寫操作,比如線程A對變量a進行寫操作,線程B對變量b進行寫操作。而由于緩存一致性(Cache coherence)協(xié)議的存在,如果其中A線程對緩存行C中變量a進行了寫操作的話,為了保證各個CPU核心的數據一致(也就是說兩個CPU核心看到了a的值是一樣的,因為a的值已經發(fā)生變化了,需要讓另外的CPU核心知道,不然另外的CPU核心使用的就是舊的值,那么程序結果就不對了),其他核心的這個緩存行就會失效,如果他還想使用這個緩存行的話就需要重新三級Cache加載,如果數據不存在三級Cache當中的話,就會從內存當中加載,而這個重新加載的過程就會很拖累程序的執(zhí)行效率,而事實上線程A寫的是變量a,線程B寫的是變量b,他們并沒有真正的有共享的數據,只是他們需要的數據在同一個緩存行當中,因此稱這種現象叫做假共享(False Sharing)。

上面我們談到了,當緩存行失效的時候會從三級Cache或者內存當中加載,而多個不同的CPU核心是共享三級Cache的(上圖當中已經顯示出來了),其中一個CPU核心更新了數據,會把數據刷新到三級Cache或者內存當中,因此這個時候其他的CPU核心去加載數據的時候就是新值了。

上面談到的關于CPU的緩存一致性(Cache coherence)的內容還是比較少的,如果你想深入了解緩存一致性(Cache coherence)和緩存一致性協(xié)議可以仔細去看這篇文章。

我們再來舉一個更加具體的例子:

假設在內存當中,變量a和變量b都占四個字節(jié),而且他們的內存地址是連續(xù)且相鄰的,現在有兩個線程A和B,線程A要不斷的對變量a進行+1操作,線程B需要不斷的對變量進行+1操作,現在這個兩個數據所在的緩存行已經被緩存到三級緩存了。

  • 線程A從三級緩存當中將數據加載到二級緩存和一級緩存然后在CPU- Core0當中執(zhí)行代碼,線程B從三級緩存將數據加載到二級緩存和一級緩存然后在CPU- Core1當中執(zhí)行代碼。
  • 線程A不斷的執(zhí)行a += 1,因為線程B緩存的緩存行當中包含數據a,線程A在修改a的值之后,就會在總線上發(fā)送消息,讓其他處理器當中含有變量a的緩存行失效,在處理器將緩存行失效之后,就會在總線上發(fā)送消息,表示緩存行已經失效,線程A所在的CPU- Core0收到消息之后將更新后的數據刷新到三級Cache。
  • 這個時候線程B所在的CPU-Core1當中含有a的緩存行已經失效,因為變量b和變量a在同一個緩存行,現在線程B想對變量b進行加一操作,但是在一級和二級緩存當中已經沒有了,它需要三級緩存當中加載這個緩存行,如果三級緩存當中沒有就需要去內存當中加載。
  • 仔細分析上面的過程你就會發(fā)現線程B并沒有對變量a有什么操作,但是它需要的緩存行就失效了,雖然和線程B共享需要同一個內容的緩存行,但是他們之間并沒有真正共享數據,所以這種現象叫做假共享。

Java代碼復現假共享

復現假共享

下面是兩個線程不斷對兩個變量執(zhí)行++操作的代碼:

class Data {
  public volatile long a;
  public volatile long b;
}
 
public class FalseSharing {
  public static void main(String[] args) throws InterruptedException {
    Data data = new Data();
    long start = System.currentTimeMillis();
    Thread A = new Thread(() -> {
      for (int i = 0;  i < 500_000_000; i++) {
        data.a += 1;
      }
    }, "A");
 
    Thread B = new Thread(() -> {
      for (int i = 0;  i < 500_000_000; i++) {
        data.b += 1;
      }
    }, "B");
    A.start();
    B.start();
    A.join();
    B.join();
    long end = System.currentTimeMillis();
    System.out.println("花費時間為:" + (end - start));
    System.out.println(data.a);
    System.out.println(data.b);
  }
}

上面的代碼比較簡單,這里就不進行說明了,上面的代碼在我的筆記本上的執(zhí)行時間大約是17秒。

上面的代碼變量a和變量b在內存當中的位置是相鄰的,他們在被CPU加載之后會在同一個緩存行當中,因此會存在假共享的問題,程序的執(zhí)行時間會變長。

下面的代碼是優(yōu)化過后的代碼,在變量a前面和后面分別加入56個字節(jié)的數據,再加上a的8個字節(jié)(long類型是8個字節(jié)),這樣a前后加上a的數據有64個字節(jié),而現在主流的緩存行是64個字節(jié),夠一個緩存行的大小,因為數據a和數據b就不會在同一個緩存行當中,因此就不會存在假共享的問題了。而下面的代碼在我筆記本當中執(zhí)行的時間大約為5秒。這就足以看出假共享會對程序的執(zhí)行帶來多大影響了。

class Data {
  public volatile long a1, a2, a3, a4, a5, a6, a7;
  public volatile long a;
  public volatile long b1, b2, b3, b4, b5, b6, b7;
  public volatile long b;
}
 
public class FalseSharing {
  public static void main(String[] args) throws InterruptedException {
    Data data = new Data();
    long start = System.currentTimeMillis();
    Thread A = new Thread(() -> {
      for (int i = 0;  i < 500_000_000; i++) {
        data.a += 1;
      }
    }, "A");
 
    Thread B = new Thread(() -> {
      for (int i = 0;  i < 500_000_000; i++) {
        data.b += 1;
      }
    }, "B");
    A.start();
    B.start();
    A.join();
    B.join();
    long end = System.currentTimeMillis();
    System.out.println("花費時間為:" + (end - start));
    System.out.println(data.a);
    System.out.println(data.b);
  }
}

JDK解決假共享

為了解決假共享的問題,JDK為我們提供了一個注解@Contened解決假共享的問題。

import sun.misc.Contended;
 
class Data {
//  public volatile long a1, a2, a3, a4, a5, a6, a7;
  @Contended
  public volatile long a;
//  public volatile long b1, b2, b3, b4, b5, b6, b7;
  @Contended
  public volatile long b;
}
 
public class FalseSharing {
  public static void main(String[] args) throws InterruptedException {
    Data data = new Data();
 
    long start = System.currentTimeMillis();
    Thread A = new Thread(() -> {
      for (long i = 0;  i < 500_000_000; i++) {
        data.a += 1;
      }
    }, "A");
 
    Thread B = new Thread(() -> {
      for (long i = 0;  i < 500_000_000; i++) {
        data.b += 1;
      }
    }, "B");
    A.start();
    B.start();
    A.join();
    B.join();
    long end = System.currentTimeMillis();
    System.out.println("花費時間為:" + (end - start));
    System.out.println(data.a);
    System.out.println(data.b);
  }
}

上面代碼的執(zhí)行時間也是5秒左右,和之前我們自己在變量的左右兩邊插入變量的效果是一樣的,但是JDK提供的這個接口和我們自己實現的還是有所區(qū)別的。(注意:上面的代碼是在JDK1.8下執(zhí)行的,如果要想@Contended注解生效,你還需要在JVM參數上加入-XX:-RestrictContended,這樣上面的代碼才能生效否則是不能夠生效的)

  • 在我們自己解決假共享的代碼當中,是在變量a的左右兩邊加入56個字節(jié)的其他變量,讓他和變量b不在同一個緩存行當中。
  • 在JDK給我們提供的注解@Contended,是在被加注解的字段的右邊加入一定數量的空字節(jié),默認加入128空字節(jié),那么變量a和變量b之間的內存地址大一點,最終不在同一個緩存行當中。這個字節(jié)數量可以使用JVM參數-XX:ContendedPaddingWidth=64,進行控制,比如這個是64個字節(jié)。
  • 除此之外@Contended注解還能夠將變量進行分組:
class Data {
  @Contended("a")
  public volatile long a;
  
  @Contended("bc")
  public volatile long b;
  @Contended("bc")
  public volatile long c;
}

在解析注解的時候會讓同一組的變量在內存當中的位置相鄰,不同的組之間會有一定數量的空字節(jié),配置方式還是跟上面一樣,默認每組之間空字節(jié)的數量為128。

比如上面的變量在內存當中的邏輯布局詳細布局如下:

OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           20 0a 06 00 (00100000 00001010 00000110 00000000) (395808)
     12   132        (alignment/padding gap)                  
    144     8   long Data.a                                    0
    152   128        (alignment/padding gap)                  
    280     8   long Data.b                                    0
    288     8   long Data.c                                    0
    296   128        (loss due to the next object alignment)
Instance size: 424 bytes
Space losses: 260 bytes internal + 128 bytes external = 388 bytes total

上面的內容是通過下面代碼打印的,你只要在pom文件當中引入包jol即可:

從更低層次C語言看假共享

前面我們是使用Java語言去驗證假共享,在本小節(jié)當中我們通過一個C語言的多線程程序(使用pthread)去驗證假共享。(下面的代碼在類Unix系統(tǒng)都可以執(zhí)行)

#include <stdio.h>
#include <pthread.h>
#include <time.h>
 
#define CHOOSE // 這里定義了 CHOOSE 如果不想定義CHOOSE 則將這一行注釋掉即可
 
// 定義一個全局變量
int data[1000];
 
void* add(void* flag) {
  // 這個函數的作用就是不斷的往 data 當中的某個數據進行加一操作
  int idx = *((int *)flag);
  for (long i = 0; i < 10000000000; ++i) {
    data[idx]++;
  }
}
 
int main() {
  pthread_t a, b;
#ifdef CHOOSE // 如果定義了 CHOOSE 則執(zhí)行下面的代碼 讓兩個線程操作的變量隔得遠一點 讓他們不在同一個緩存行當中
  int flag_a = 0;
  int flag_b = 100;
  printf("遠離\n");
#else // 如果沒有定義 讓他們隔得近一點 也就是說讓他們在同一個緩存行當中
  int flag_a = 0;
  int flag_b = 1;
  printf("臨近\n");
#endif
  pthread_create(&a, NULL, add, &flag_a); // 創(chuàng)建線程a 執(zhí)行函數 add 傳遞參數 flag_a 并且啟動
  pthread_create(&b, NULL, add, &flag_b); // 創(chuàng)建線程b 執(zhí)行函數 add 傳遞參數 flag_b 并且啟動
  long start = time(NULL);
  pthread_join(a, NULL); // 主線程等待線程a執(zhí)行完成
  pthread_join(b, NULL); // 主線程等待線程b執(zhí)行完成
  long end = time(NULL);
  printf("data[0] = %d\t data[1] = %d\n", data[0], data[1]);
  printf("cost time = %ld\n", (end - start));
  return 0;
}

上面代碼的輸出結果如下圖所示:

我們首先來解釋一下上面time命令的輸出:

  • readl:這個表示真實世界當中的墻鐘時間,就是表示這個程序執(zhí)行所花費的時間,這個秒單位和我們平常說的秒是一樣的。
  • user:這個表示程序在用戶態(tài)執(zhí)行的CPU時間,CPU時間和真實時間是不一樣的,這里需要注意區(qū)分,這里的秒和我們平常的秒是不一樣的。
  • sys:這個表示程序在內核態(tài)執(zhí)行所花費的CPU時間。

從上面程序的輸出結果我們可以很明顯的看出來當操作的兩個整型變量相隔距離遠的時候,也就是不在同一個緩存行的時候,程序執(zhí)行的速度是比數據隔得近在同一個緩存行的時候快得多,這也從側面顯示了假共享很大程度的降低了程序執(zhí)行的效率。

總結

在本篇文章當中主要討論了以下內容:

  • 當多個線程操作同一個緩存行當中的多個不同的變量時,雖然他們事實上沒有對數據進行共享,但是他們對同一個緩存行當中的數據進行修改,而由于緩存一致性協(xié)議的存在會導致程序執(zhí)行的效率降低,這種現象叫做假共享。
  • 在Java程序當中我們如果想讓多個變量不在同一個緩存行當中的話,我們可以在變量的旁邊通過增加其他變量的方式讓多個不同的變量不在同一個緩存行。
  • JDK也為我們提供了Contended注解可以在字段的后面通過增加空字節(jié)的方式讓多個數據不在同一個緩存行,而且你需要在JVM參數當中加入-XX:-RestrictContended,同時你可以通過JVM參數-XX:ContendedPaddingWidth=64調整空字節(jié)的數目。JDK8之后注解Contended在JDK當中的位置有所變化,大家可以查詢一下。
  • 我們也是用了C語言的API去測試了假共享,事實上在Java虛擬機當中底層的線程也是通過調用pthread_create進行創(chuàng)建的。

到此這篇關于Java并發(fā)程序刺客之假共享的原理及復現的文章就介紹到這了,更多相關Java并發(fā) 假共享內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • SpringBoot中整合MyBatis-Plus的方法示例

    SpringBoot中整合MyBatis-Plus的方法示例

    這篇文章主要介紹了SpringBoot中整合MyBatis-Plus的方法示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-09-09
  • 關于@Scheduled參數及cron表達式解釋

    關于@Scheduled參數及cron表達式解釋

    這篇文章主要介紹了關于@Scheduled參數及cron表達式解釋,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • 解決Mybatis中mapper.xml文件update,delete及insert返回值問題

    解決Mybatis中mapper.xml文件update,delete及insert返回值問題

    這篇文章主要介紹了解決Mybatis中mapper.xml文件update,delete及insert返回值問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-11-11
  • JavaWeb文件上傳下載功能示例解析

    JavaWeb文件上傳下載功能示例解析

    這篇文章主要介紹了JavaWeb中的文件上傳和下載功能的實現,文件上傳和下載功能是非常常用的功能,需要的朋友可以參考下
    2016-06-06
  • SpringBoot超詳細講解yaml配置文件

    SpringBoot超詳細講解yaml配置文件

    這篇文章主要介紹了SpringBoot中的yaml配置文件問題,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-06-06
  • 舉例講解Java的RTTI運行時類型識別機制

    舉例講解Java的RTTI運行時類型識別機制

    這篇文章主要介紹了Java的RTTI運行時類型識別機制,包括泛化的Class引用以及類型檢查instanceof等知識點,需要的朋友可以參考下
    2016-05-05
  • SpringBoot中配置Web靜態(tài)資源路徑的方法

    SpringBoot中配置Web靜態(tài)資源路徑的方法

    這篇文章主要介紹了SpringBoot中配置Web靜態(tài)資源路徑的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-09-09
  • Java8中的默認方法(面試者必看)

    Java8中的默認方法(面試者必看)

    這篇文章主要介紹了Java8中的默認方法(面試者必看),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-11-11
  • java數據結構與算法之簡單選擇排序詳解

    java數據結構與算法之簡單選擇排序詳解

    這篇文章主要介紹了java數據結構與算法之簡單選擇排序,結合實例形式分析了選擇排序的原理、實現方法與相關操作技巧,需要的朋友可以參考下
    2017-05-05
  • Java將網絡圖片轉成輸入流以及將url轉成InputStream問題

    Java將網絡圖片轉成輸入流以及將url轉成InputStream問題

    這篇文章主要介紹了Java將網絡圖片轉成輸入流以及將url轉成InputStream問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-01-01

最新評論