多線程中的volatile和偽共享

? 偽共享 false sharing,顧名思義,“偽共享”就是“其實不是共享”。那什么是“共享”?多CPU同時訪問同一塊內存區域就是“共享”,就會產生沖突,需要控制協議來協調訪問。會引起“共享”的最小內存區域大小就是一個cache line。因此,當兩個以上CPU都要訪問同一個cache line大小的內存區域時,就會引起沖突,這種情況就叫“共享”。但是,這種情況里面又包含了“其實不是共享”的“偽共享”情況。比如,兩個處理器各要訪問一個word,這兩個word卻存在于同一個cache line大小的區域里,這時,從應用邏輯層面說,這兩個處理器并沒有共享內存,因為他們訪問的是不同的內容(不同的word)。但是因為cache line的存在和限制,這兩個CPU要訪問這兩個不同的word時,卻一定要訪問同一個cache line塊,產生了事實上的“共享”。顯然,由于cache line大小限制帶來的這種“偽共享”是我們不想要的,會浪費系統資源。

  緩存系統中是以緩存行(cache line)為單位存儲的。緩存行是2的整數冪個連續字節,一般為32-256個字節。最常見的緩存行大小是64個字節。當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享。緩存行上的寫競爭是運行在SMP系統中并行線程實現可伸縮性最重要的限制因素。有人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現偽共享。

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

  圖1說明了偽共享的問題。在核心1上運行的線程想更新變量X,同時核心2上的線程想要更新變量Y。不幸的是,這兩個變量在同一個緩存行中。每個線程都要去競爭緩存行的所有權來更新變量。如果核心1獲得了所有權,緩存子系統將會使核心2中對應的緩存行失效。當核心2獲得了所有權然后執行更新操作,核心1就要使自己對應的緩存行失效。這會來來回回的經過L3緩存,大大影響了性能。如果互相競爭的核心位于不同的插槽,就要額外橫跨插槽連接,問題可能更加嚴重。

  Java Memory Layout Java內存布局,在項目開發中,大多使用HotSpot的JVM,hotspot中對象都有兩個字(四字節)長的對象頭。第一個字是由24位哈希碼和8位標志位(如鎖的狀態或作為鎖對象)組成的Mark Word。第二個字是對象所屬類的引用。如果是數組對象還需要一個額外的字來存儲數組的長度。每個對象的起始地址都對齊于8字節以提高性能。因此當封裝對象的時候為了高效率,對象字段聲明的順序會被重排序成下列基于字節大小的順序:

?

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

?

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

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

?

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;/*** 為了展示其性能影響,我們啟動幾個線程,每個都更新它自己獨立的計數器。計數器是volatile long類型的,所以其它線程能看到它們的進展。*/public final static class VolatileLong {/* 用volatile[?vɑ:l?tl]修飾的變量,線程在每次使用變量的時候,JVM虛擬機只保證從主內存加載到線程工作內存的值是最新的 */public volatile long value = 0L;/* 緩沖行填充 *//* 37370571461 :不使用緩沖行執行納秒數 *//* 16174480826 :使用緩沖行執行納秒數,性能提高一半 */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會布局在內存的什么位置。它們是獨立的對象。但是經驗告訴我們同一時間分配的對象趨向集中于一塊。*/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();}}/** 為了展示其性能影響,我們啟動幾個線程,每個都更新它自己獨立的計數器。計數器是volatile long類型的,所以其它線程能看到它們的進展*/@Overridepublic void run() {long i = ITERATIONS + 1;while (0 != --i) {longs[arrayIndex].value = i;}}
}

?

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

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

?

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環境下,緩存行填充終于被JAVA原生支持了。JAVA 8中添加了一個@Contended的注解,添加這個的注解,將會在自動進行緩存行填充。以上的例子可以改為:

?

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;
}

?

?

執行時,必須加上虛擬機參數-XX:-RestrictContended,@Contended注釋才會生效。很多文章把這個漏掉了,那樣的話實際上就沒有起作用。

?

補充:

byte字節 ?bit位 1byte=8bit

volatile說明

package basic;public class TestVolatile {public static int count = 0;/* 即使使用volatile,依舊沒有達到我們期望的效果 */// public volatile static int count = 0;public static void increase() {try {// 延遲10毫秒,使得結果明顯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("期望運行結果:10000");System.out.println("實際運行結果:" + TestVolatile.count);}
}

volatile關鍵字的使用:用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改后的最新值。但是由于操作不是原子性的,對于volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工作內存的值是最新的。

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

?

  • read and load 從主存復制變量到當前工作內存
  • use and assign? 執行代碼,改變共享變量值(其中use and assign 可以多次出現)?
  • store and write 用工作內存數據刷新主存相關內容

但是這些操作并不是原子性,也就是在read load之后,如果主內存count變量發生修改之后,線程工作內存中的值由于已經加載,不會產生對應的變化,所以計算出來的結果會和預期不一樣。對于volatile修飾的變量,JVM虛擬機只是保證從主內存加載到線程工作內存的值是最新的。例如假如線程1,線程2在進行read load操作中,發現主內存中count的值都是5,那么都會加載這個最新的值。在線程1堆count進行修改之后,會write到主內存中,主內存中的count變量就會變為6。線程2由于已經進行read,load操作,在進行運算之后,也會更新主內存count的變量值為6。導致兩個線程即使使用volatile關鍵字修改之后,還是會存在并發的情況。

對于volatile修飾的變量,JVM虛擬機只能保證從主內存加載到線程工作內存的值是最新的。

參考博客:

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

?

轉載于:https://www.cnblogs.com/RunForLove/p/5624390.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/374443.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/374443.shtml
英文地址,請注明出處:http://en.pswp.cn/news/374443.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C語言代碼規范(一)縮進與換行

一、縮進的空格數為4個。最好配置代碼編輯器將TAB鍵設置為空格替換&#xff0c;避免出現另一個編輯器打開時格式變亂的情況。 例如Notepad設置 KEIL設置 二、“{” 和 “}”各自獨占一行。 不規范例子&#xff1a; for(i 0; i < student_num; i) { if((score[i] > 0…

armv7 cortex a系列編程手冊_AWTK能為現代GUI編程帶來何種改變?

AWTK是一個伸縮性極強的嵌入式圖形框架&#xff0c;它的誕生會給GUI編程研發工程師帶來哪些改變&#xff1f;AWTK是一個伸縮性極強的嵌入式圖形框架&#xff0c;可在Cortex-M3這樣低端的單片機上運行&#xff0c;也可以在Cortex-A7/A8/A9等處理器&#xff0c;甚至DSP以及X86處理…

【轉】各種概念POJO、JAVABEAN、DAO、DTO、PO、VO、BO、SSH、EJB

POJO&#xff08;pure old java object&#xff09; 是普通java類&#xff0c;有一些private的參數作為對象的屬性&#xff0c;然后針對每一個參數定義get和set方法訪問的接口。我看到這個定義&#xff0c;心里就有個疑問了&#xff0c;這個POJO跟JavaBean的定義怎么就這么像&a…

為什么要編寫單元測試–測試技巧8

我對最近在“您應該測試什么”上的博客有很多反應&#xff0c;有些人出于各種原因同意我的想法&#xff0c;另一些人則認為建議某些類可能不需要單元測試是非常危險的。 已經處理了什么測試&#xff0c;今天的博客涉及為什么要編寫單元測試&#xff0c;而今天的示例代碼是基于一…

Git遷移 從SVN到Git

Migrating from SVN to Git 首先我們需要在Stach或者GitHub上新建一個Repository, 拿到它的URL。 接下來參照如下步驟 : At first we should create a new git repository at Stash and get the repository URL, and then follow below steps: 1. 切換到本地git工作目錄 chang…

C語言代碼規范(二)空格

一、逗號, 之后加空格 printf("error! score[%d] %d\n", i, score[i]); 二、分號; 之后加空格 for(i 0; i < student_num; i) 三、關系運算符<、<、>、>、、! 前后加空格 if( (score[i] > 0) && (score[i] < 100) ) 四、賦值運算符…

c++ 多重背包狀態轉移方程_動態規劃入門——詳解經典問題零一背包

本文始發于個人公眾號&#xff1a;TechFlow&#xff0c;原創不易&#xff0c;求個關注今天是周三算法與數據結構專題的第12篇文章&#xff0c;動態規劃之零一背包問題。在之前的文章當中&#xff0c;我們一起探討了二分、貪心、排序和搜索算法&#xff0c;今天我們來看另一個非…

Discuz! 的編碼規范

前言 本規范由編程原則組成&#xff0c;融合并提煉了開發人員長時間積累下來的成熟經驗&#xff0c;意在幫助形成良好一致的編程風格。適用范圍 如無特殊說明&#xff0c;以下規則要求完全適用于Discuz!項目&#xff0c;同時也可大部分適用于COMSENZ旗下其他PHP項目。標準化的重…

C語言代碼規范(三)if語句

一、整型變量與0比較 許多人為了一時之便&#xff0c;模仿布爾變量風格寫為如下代碼 if(value) {... }if(!value) {... } 應當用 或 ! 來與0比較 if(0 value) {... }if(0 ! value) {... } 二、當if內的語句是與常量進行比較時&#xff0c;常量為左值&#xff0c;變量為右…

6月24 面向對象的設計原則-----工廠模式和單列模式

工廠模式&#xff1a; 工廠模式就是專門負責將大量有共同接口的類實例化&#xff0c;而且不必事先知道每次是要實例化哪一個類的模式。它定義一個用于創建對象的接口&#xff0c;由子類決定實例化哪一個類。 工廠模式相當于創建實例對象的new&#xff0c;經常要根據類Class生成…

LeetCode Subsets

原題鏈接在這里&#xff1a;https://leetcode.com/problems/subsets/ 題目&#xff1a; Given a set of distinct integers, nums, return all possible subsets. Note: Elements in a subset must be in non-descending order.The solution set must not contain duplicate su…

使用ThreadPoolExecutor并行化獨立的單線程任務

Java SE 5.0中引入的任務執行框架是簡化多線程應用程序的設計和開發的巨大飛躍。 該框架提供了用于管理任務概念&#xff0c;管理線程生命周期及其執行策略的工具。 在此博客文章中&#xff0c;我們將描述該框架的功能&#xff0c;靈活性和簡單性&#xff0c;以展示一個簡單的用…

python定義一個圓_Python-矩形和圓形

原博文 2019-11-11 12:34 ? Exercise 15.1. 定義一個叫做Circle 類&#xff0c;類的屬性是圓心 (center) 和半徑 (radius) , 其中&#xff0c;圓心 (center) 是一個 Point 類&#xff0c;而半徑 (radius) 是一個數字。 實例化一個圓心 (center) 為 (150, 100) &#xff0c;半…

C語言代碼規范(四)命名規則

一、宏定義全部字母大寫&#xff0c;單詞間下劃線間隔 #define FLASH_PAGE_SIZE 256 #define FLASH_SECTOR_SIZE (4 * 1024) #define FLASH_BLOCK_SIZE (64 * 1024) #define FLASH_SIZE (16 * 1024 * 1024) 二、const修飾的常量全部字母大寫&#xff0c;單詞間…

Forbidden You don't have permission to access / on this server PHP

Forbidden You dont have permission to access / on this server PHP 在新安裝的谷歌游覽器里&#xff0c;打不了PHP網站了&#xff0c;錯誤顯示&#xff1a; Forbidden You dont have permission to access / on this server. 原因還是配置權限問題 解決辦法&#xff1a; wa…

Spring 3.1和JPA的持久層

1.概述 本教程顯示了如何使用Hibernate作為持久性提供程序使用JPA設置Spring 。 有關使用基于Java的配置和項目的基本Maven pom設置Spring上下文的分步介紹&#xff0c;請參閱本文 。 2. Java的JPA Spring配置 要在Spring項目中使用JPA&#xff0c; 需要設置EntityManager 。…

150928錯誤認識

1. $arr array(); foreach ($re as $k>$v){  $arr[] $v[updatetime];} $arr的返回結果為&#xff1a; Array ([0] > 2014-09[1] > 2015-04[2] > 2015-09 )$arr array(); foreach ($re as $k>$v){  $arr[$k] $v[updatetime];} $arr的返回結果為&#xff…

STM32F1筆記(一)GPIO輸出

GPIO&#xff1a;General Purpose Input Output &#xff08;通用輸入/輸出&#xff09;。 GPIO最經典應用&#xff1a;LED燈。 先看電路。聲明&#xff1a;參考正點原子戰艦開發板。 與LED串聯的電阻稱為限流電阻。 限流電阻計算公式&#xff1a;R(U-LED壓降)/20ma。 U為LE…

dataframe轉化為array_【Python專欄】12 種高效 Numpy 和 Pandas 函數為你加速分析

來源&#xff1a;機器之心編譯&#xff1a;Jamin、杜偉、張倩我們都知道&#xff0c;Numpy 是 Python 環境下的擴展程序庫&#xff0c;支持大量的維度數組和矩陣運算&#xff1b;Pandas 也是 Python 環境下的數據操作和分析軟件包&#xff0c;以及強大的數據分析庫。二者在日常…

具有GlassFish和一致性的高性能JPA –第1部分

您以前聽說過連貫性嗎&#xff1f; 大概是。 它是那些著名的內存網格解決方案之一&#xff0c;該解決方案承諾了超快的數據訪問速度和對經常使用的數據的無限空間。 一些眾所周知的競爭對手是Infinispan &#xff0c; Memcached和Terracotta Ehcache 。 它們都很棒&#xff0c;…