volatile CAS

減小上下文切換的方法有無鎖併發編程、CAS算法、使用最少線程和使用協程。

無鎖併發編程。多線程競爭鎖時,會引發上下文切換,因此多線程處理數據時,能夠用一
些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不一樣的線程處理不一樣段的數據。
協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。


如今咱們介紹避免死鎖的幾個常見方法。
·避免一個線程同時獲取多個鎖。
·避免一個線程在鎖內同時佔用多個資源,儘可能保證每一個鎖只佔用一個資源。
·嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
·對於數據庫鎖,加鎖和解鎖必須在一個數據庫鏈接裏,不然會出現解鎖失敗的狀況。

硬件資源限 制有帶寬的上傳/下載速度、硬盤讀寫速度和CPU的處理速度。軟件資源限制有數據庫的鏈接
數和socket鏈接數等。


Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節
碼,最終須要轉化爲彙編指令在CPU上執行,Java中所使用的併發機制依賴於JVM的實現CPU的指令。


volatile是輕量級的 synchronized,它在多處理器開發中保證了共享變量的「可見性」。可見性的意思是當一個線程
修改一個共享變量時,另一個線程能讀到這個修改的值。若是volatile變量修飾符使用恰當
的話,它比synchronized的使用和執行成本更低,由於它不會引發線程上下文的切換和調度

x = 10; //語句1
y = x; //語句2
x++; //語句3
x = x + 1; //語句4
咋一看,有些朋友可能會說上面的4個語句中的操做都是原子性操做。其實只有語句1是原子性操做,其餘三個語句都不是原子性操做。
  語句1是直接將數值10賦值給x,也就是說線程執行這個語句的會直接將數值10寫入到工做內存中。
  語句2實際上包含2個操做,它先要去讀取x的值,再將x的值寫入工做內存,雖然讀取x的值以及 將x的值寫入工做內存 這2個操做都是原子性操做,可是合起來就不是原子性操做了。
  一樣的,x++和 x = x+1包括3個操做:讀取x的值,進行加1操做,寫入新的值。
  因此上面4個語句只有語句1的操做具有原子性。
  也就是說,只有簡單的讀取、賦值(並且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操做)纔是原子操做。
  不過這裏有一點須要注意:在32位平臺下,對64位數據的讀取和賦值是須要經過兩個操做來完成的,不能保證其原子性。可是好像在最新的JDK中,JVM已經保證對64位數據的讀取和賦值也是原子性操做了。
  從上面能夠看出,Java內存模型只保證了基本讀取和賦值是原子性操做,若是要實現更大範圍操做的原子性,可以經過synchronized和Lock來實現。因爲synchronized和Lock可以保證任一時刻只有一個線程執行該代碼塊,那麼天然就不存在原子性問題了,從而保證了原子性。java

 


2. 線程獨有的工做內存和進程內存(主內存)之間經過8中原子操做來實現,以下圖所示:c++

原子操做的規則(部分):
1) read,load必須連續執行,可是不保證原子性。
2) store,write必須連續執行,可是不保證原子性。
3) 不能丟失變量最後一次assign操做的副本,即遍歷最後一次assign的副本必需要回寫到MainMemory中。
其它規則詳見《深刻理解Java虛擬機》第12章 Java內存模型與線程
read(讀取) :它把一個變量的值從主內存中傳遞到工做內存,
load(載入) :賦值給工做內存
store(存儲) :把工做內存中的值傳遞到主內存中來
write(寫入) :賦值給主內存
use(使用) :使用工做變量值
assign(賦值) :修改工做變量算法


你們都知道,計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程當中,勢必涉及到數據的讀取和寫入。因爲程序運行過程當中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,因爲CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,所以若是任什麼時候候對數據的操做都要經過和內存的交互來進行,會大大下降指令執行的速度。所以在CPU裏面就有了高速緩存
 也就是,當程序在運行過程當中,會將運算須要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束以後,再將高速緩存中的數據刷新到主存當中。舉個簡單的例子,好比下面的這段代碼:
i = i + 1;
當線程執行這個語句時,會先從主存當中讀取i的值,而後複製一份到高速緩存當中,而後CPU執行指令對i進行加1操做,而後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。CPU只跟高速緩存交互。數據庫

這個代碼在單線程中運行是沒有任何問題的,可是在多線程中運行就會有問題了。在多核CPU中,每條線程可能運行於不一樣的CPU中,所以每一個線程運行時有本身的高速緩存(對單核CPU來講,其實也會出現這種問題,只不過是以線程調度的形式來分別執行的)。本文咱們以多核CPU爲例。
  好比同時有2個線程執行這段代碼,假如初始時i的值爲0,那麼咱們但願兩個線程執行完以後i的值變爲2。可是事實會是這樣嗎?
  可能存在下面一種狀況:初始時,兩個線程分別讀取i的值存入各自所在的CPU的高速緩存當中,而後線程1進行加1操做,而後把i的最新值1寫入到內存。此時線程2的高速緩存當中i的值仍是0,進行加1操做以後,i的值爲1,而後線程2把i的值寫入內存。
  最終結果i的值是1,而不是2。這就是著名的緩存一致性問題。一般稱這種被多個線程訪問的變量爲共享變量。
也就是說,若是一個變量在多個CPU中都存在緩存(通常在多線程編程時纔會出現),那麼就可能存在緩存不一致的問題。
  爲了解決緩存不一致性問題,一般來講有如下2種解決方法:
  1)經過在總線加LOCK#鎖的方式
  2)經過緩存一致性協
  這2種方式都是硬件層面上提供的方式。
在早期的CPU當中,是經過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。由於CPU和其餘部件進行通訊都是經過總線來進行的,若是對總線加LOCK#鎖的話,也就是說阻塞了其餘CPU對其餘部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。好比上面例子中 若是一個線程在執行 i = i +1,若是在執行這段代碼的過程當中,在總線上發出了LCOK#鎖的信號,那麼只有等待這段代碼徹底執行完畢以後,其餘CPU才能從變量i所在的內存讀取變量,而後進行相應的操做。這樣就解決了緩存不一致的問題。編程

可是上面的方式會有一個問題,因爲在鎖住總線期間,其餘CPU沒法訪問內存,致使效率低下。
  因此就出現了緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。(這是沒有涉及到java的內存的模型)緩存

 


i = 9;
倘若一個線程執行到這個語句時,我暫且假設爲一個32位的變量賦值包括兩個過程:爲低16位賦值,爲高16位賦值。
那麼就可能發生一種狀況:當將低16位數值寫入以後,忽然被中斷,而此時又有一個線程去讀取i的值,那麼讀取到的就是錯誤的數據。這就是原子性。多線程

//線程1執行的代碼                                            //線程2執行的代碼
int i = 0;                                                             j = i;
i = 10;架構

倘若執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i =10這句時,會先把i的初始值加載到CPU1的高速緩存中,而後賦值爲10,那麼在CPU1的高速緩存當中i的值變爲10了,卻沒有當即寫入到主存當中。
此時線程2執行 j = i,它會先去主存讀取i的值並加載到CPU2的緩存當中,注意此時內存當中i的值仍是0,那麼就會使得j的值爲0,而不是10.
這就是可見性問題,線程1對變量i修改了以後,線程2沒有當即看到線程1修改的值。併發

int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2app

 

上面代碼定義了一個int型變量,定義了一個boolean類型變量,而後分別對兩個變量進行賦值操做。從代碼順序上看,語句1是在語句2前面的,那麼JVM在真正執行這段代碼的時候會保證語句1必定會在語句2前面執行嗎?不必定,爲何呢?這裏可能會發生指令重排序(Instruction Reorder)。
  下面解釋一下什麼是指令重排序,通常來講,處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。單線程下沒影響就排序。
  好比上面的代碼中,語句1和語句2誰先執行對最終的程序結果並無影響,那麼就有可能在執行過程當中,語句2先執行而語句1後執行。
  可是要注意,雖然處理器會對指令進行重排序,可是它會保證程序最終結果會和代碼順序執行結果相同,那麼它靠什麼保證的呢?再看下面一個例子:

int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
這段代碼有4個語句,那麼可能的一個執行順序是:2134,那麼可不多是這個執行順序呢: 語句2 語句1 語句4 語句3
  不可能,由於處理器在進行重排序時是會考慮指令之間的數據依賴性,若是一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2以前執行。
  雖然重排序不會影響單個線程內程序執行的結果,可是多線程呢?下面看一個例子:

//線程1:
context = loadContext(); //語句1
inited = true; //語句2

//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代碼中,因爲語句1和語句2沒有數據依賴性,所以可能會被重排序。假如發生了重排序,在線程1執行過程當中先執行語句2,而此是線程2會覺得初始化工做已經完成,那麼就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context並無被初始化,就會致使程序出錯。
  從上面能夠看出,指令重排序不會影響單個線程的執行,可是會影響到線程併發執行的正確性。
  也就是說,要想併發程序正確地執行,必需要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會致使程序運行不正確。


 三.Java內存模型

Java內存模型規定全部的變量都是存在主存當中(相似於前面說的物理內存),每一個線程都有本身的工做內存(相似於前面的高速緩存)。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。而且每一個線程不能訪問其餘線程的工做內存。
  舉個簡單的例子:在java中,執行下面這個語句:i  = 10;

執行線程必須先在本身的工做線程中對變量i所在的緩存行進行賦值操做,而後再寫入主存當中。而不是直接將數值10寫入主存當中。
  那麼Java語言 自己對 原子性、可見性以及有序性提供了哪些保證呢?

synchronized和Lock保證每一個時刻是有一個線程執行同步代碼,至關因而單線程執行同步代碼,(多線程就是經過單線程解決的)天然就保證了有序性。 另外,Java內存模型具有一些先天的「有序性」,即不須要經過任何手段就可以獲得保證的有序性,這個一般也稱爲 happens-before 原則。若是兩個操做的執行次序沒法從happens-before(不能重排序的原則)原則推導出來,那麼它們就不能保證它們的有序性,虛擬機能夠隨意地對它們進行重排序。

volatile關鍵字能保證可見性沒有錯,可是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的操做的原子性。

CAS其實是利用處理器提供的CMPXCHG指令實現的,而處理器執行CMPXCHG指令是一個原子性操做。

 public class Test {    
public volatile int inc = 0;         
public void increase() {        inc++;    }         
public static void main(String[] args) {        
final Test test = new Test();        
for(int i=0;i<10;i++){            
new Thread(){                
public void run() {                    
for(int j=0;j<1000;j++)                       
test.increase();                
};            
}.start();       
}                 
while(Thread.activeCount()>1)  //保證前面的線程都執行完            
Thread.yield();        
System.out.println(test.inc);    
}}

你們想一下這段程序的輸出結果是多少?也許有些朋友認爲是10000。可是事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。
  可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操做,因爲volatile保證了可見性,那麼在每一個線程中對inc自增完以後,在其餘線程中都能看到修改後的值啊,因此有10個線程分別進行了1000次操做,那麼最終inc的值應該是1000*10=10000。
  這裏面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,可是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的操做的原子性。
  在前面已經提到過,自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,就有可能致使下面這種狀況出現:
  假如某個時刻變量inc的值爲10,
  線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了;
  而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2的工做內存中緩存變量inc的緩存行無效,因此線程2會直接去主存讀取inc的值,發現inc的值時10,而後進行加1操做,並把11寫入工做內存,最後寫入主存。
  而後線程1接着進行加1操做,因爲已經讀取了inc的值,注意此時在線程1的工做內存中inc的值仍然爲10,因此線程1對inc進行加1操做後inc的值爲11(是加一操做,不是讀取,若是++的三步裏面有讀取,就會失效,下次訪問相同內存地址時就會失效),而後將11寫入工做內存,最後寫入主存。
  那麼兩個線程分別進行了一次自增操做後,inc只增長了1。
  解釋到這裏,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?而後其餘線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規則中的volatile變量規則,可是要注意,線程1對變量進行讀取操做以後,被阻塞了的話,並無對inc值進行修改。而後雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,可是線程1沒有進行修改,因此線程2根本就不會看到修改的值
  根源就在這裏,自增操做不是原子性操做,並且volatile也沒法保證對變量的任何操做都是原子性的。x = x + n,就依賴當前值,就不原子。由於讀取值以後,卡住了再次回來時候不用再次讀取值,就不會失效繼續使用舊的錯誤值
  把上面的代碼改爲如下任何一種均可以達到效果:

 

先看一段代碼,假如線程1先執行,線程2後執行:
//線程1
boolean stop = false;
while(!stop){
doSomething();
}

//線程2
stop = true;

每一個線程在運行過程當中都有本身的工做內存,那麼線程1在運行的時候,會將stop變量的值拷貝一份放在本身的工做內存當中。
那麼當線程2更改了stop變量的值以後,可是還沒來得及寫入主存當中,線程2轉去作其餘事情了,那麼線程1因爲不知道線程2對stop變量的更改,所以還會一直循環下去。
可是用volatile修飾以後就變得不同了:
  第一:使用volatile關鍵字會強制將修改的值當即寫入主存;
  第二:使用volatile關鍵字的話,當線程2進行修改時,會致使線程1的工做內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);
  第三:因爲線程1的工做內存中緩存變量stop的緩存行無效,因此線程1再次讀取變量stop的值時會去主存讀取
  那麼在線程2修改stop值時(固然這裏包括2個操做,修改線程2工做內存中的值,而後將修改後的值寫入內存),會使得線程1的工做內存中緩存變量stop的緩存行無效,而後線程1讀取(下次訪問相同內存地址時)時,發現本身的緩存行無效,它會等待緩存行對應的主存地址被更新以後,而後去對應的主存讀取最新的值。
  那麼線程1讀取到的就是最新的正確的值。

 

3.volatile能保證有序性嗎?
  在前面提到volatile關鍵字能禁止指令重排序,因此volatile能在必定程度上保證有序性。
  volatile關鍵字禁止指令重排序有兩層意思:
  1)當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;
  2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。
  可能上面說的比較繞,舉個簡單的例子:

//x、y爲非volatile變量
//flag爲volatile變量 
x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

因爲flag變量爲volatile變量,那麼在進行指令重排序的過程的時候,不會將語句3放到語句一、語句2前面,也不會講語句3放到語句四、語句5後面。可是要注意語句1和語句2的順序、語句4和語句5的順序是不做任何保證的。語句就是欄杆,重排序不能超過這個欄杆。
  而且volatile關鍵字能保證,執行到語句3時,語句1和語句2一定是執行完畢了的,且語句1和語句2的執行結果對語句三、語句四、語句5是可見的。
  那麼咱們回到前面舉的一個例子:

//線程1:
context = loadContext();   //語句1
inited = true;             //語句2 

//線程2:while(!inited ){  
sleep()
}doSomethingwithconfig(context);

前面舉這個例子的時候,提到有可能語句2會在語句1以前執行,那麼久可能致使context還沒被初始化,而線程2中就使用未初始化的context去進行操做,致使程序出錯。
  這裏若是用volatile關鍵字對inited變量進行修飾,就不會出現這種問題了,由於當執行到語句2時,一定能保證context已經初始化完畢。

多線程訪問時候,要加volatile,既能保證可見性又能保證不排序,不能保證原子性。

 

「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」
  lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
  1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;
  2)它會強制將對緩存的修改操做當即寫入主存
  3)若是是寫操做,它會致使其餘CPU中對應的緩存行無效

 

而volatile關鍵字在某些狀況下性能要優於synchronized,可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。一般來講,使用volatile必須具有如下2個條件:
  1)對變量的寫操做不依賴於當前值,x = x + n,就依賴當前值,就不原子。由於讀取值以後,卡住了,再次回來時候,不用再次讀取值,就不會失效,繼續使用舊的錯誤值。
  2)該變量沒有包含在具備其餘變量的不變式中
  實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。
  事實上,個人理解就是上面的2個條件須要保證操做是原子性操做才能保證使用volatile關鍵字的程序在併發時可以正確執行。

工做內存中的變量在沒有執行過assign修改值操做時,不容許無心義的同步回主內存

下面列舉幾個Java中使用volatile的幾個場景。

1.狀態標記量

volatile boolean flag = false; 
while(!flag){    
doSomething();
} 
public void setFlag() {    
flag = true;
};
volatile boolean inited = false;
//線程1:
context = loadContext();  
inited = true;             

//線程2:
while(!inited ){
sleep()
}doSomethingwithconfig(context);

2.double check

class Singleton{    
private volatile static Singleton instance = null;         
private Singleton() {             }         
public static Singleton getInstance() {        
if(instance==null) {            
synchronized (Singleton.class) {                
if(instance==null)                    
instance = new Singleton();            
}        
}        
return instance;    
}
}

Volatile歷來就不是用來保證操做原子性的關鍵字,他只負責保證可見性有序性,他的原子性是須要依靠鎖來保證的。其實他也有必定的原子性,單個volatile變量的讀操做和寫操做是具備原子性的,可是一旦擁有多個操做,再也不保證原子性。因此Volatile的使用須要你參照具體的場景來使用,並非什麼場景都能用,它是不能替代鎖的做用的。之因此稱之爲輕量級鎖,就是由於這個!

cpu指令是原子性的,就不會被打斷,i++是3個操做,因此不是原子的,i=3是原子的

至於爲何線程1的inc沒有被更新的緣由,我來談談本身的見解。
首先要明白volatile的特殊規則「保證了新值可以當即同步到主內存中,以及每次使用前當即從主存中刷新」。注意這個關鍵詞「每次使用前」。非原子性的自增操做的一次使用包括三個步驟:一、inc副本壓入操做數棧 二、加1操做 三、彈出操做數棧。只有完成這三步纔算是一次使用。當線程1把值爲10 的inc讀入工做內存中開始使用volatile變量時線程1阻塞,等到線程2成功將inc變爲11後,喚醒線程1,此時線程1仍然處於上次使用的過程當中,繼續完成本次使用,這就是爲何不從主存中更新inc的緣由。
而對於原子性操做來講,其一次使用的過程是不會被中斷的,對於另外一個例子,布爾型變量stop,當線程2將stop置爲true,當即更新到主存後,線程1再次使用到stop時便會從主存中刷新。

個人見解是: 由於線程1已經讀取到了值, 並把操做數放入了本身的操做數棧中, 此時線程1中斷了, CPU因爲保存了上次線程1的工做狀態, 所以, 輪到線程1工做時, 會繼續上次的操做, 即: 開始對操做數棧中的數進行+1操做, 而後當即刷回主存, 所以再也不涉及讀操做, 不然CPU保存線程的工做狀態將毫無心義.

在32位系統中,long和double是採用了高低位兩個位置進行操做的。在操做系統級別就須要兩個動做才能完成,因此不是原子性的

volatile修飾的變量,賦值的時候若是是一個動做就多個線程均可以看到更新值(可見性和原子性),若是是多個動做就多個線程不必定都看到更新值。

 

/** 隊列中的頭部節點 */
private transient f?inal PaddedAtomicReference<QNode> head;
/** 隊列中的尾部節點 */
private transient f?inal PaddedAtomicReference<QNode> tail;
static f?inal class PaddedAtomicReference <T> extends AtomicReference T> {
// 使用不少4個字節的引用追加到64個字節
Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
PaddedAtomicReference(T r) {
    super(r);
    }
}
public class AtomicReference <V> implements java.io.Serializable {
private volatile V value;
    // 省略其餘代碼

追加字節能優化性能?這種方式看起來很神奇,但若是深刻理解處理器架構就能理解其
中的奧祕。讓咱們先來看看LinkedTransferQueue這個類,它使用一個內部類類型來定義隊列的
頭節點(head)和尾節點(tail),而這個內部類PaddedAtomicReference相對於父類
AtomicReference只作了一件事情,就是將共享變量追加到64字節。咱們能夠來計算下,一個對
象的引用佔4個字節,它追加了15個變量(共佔60個字節),再加上父類的value變量,一共64
字節。

爲何追加64字節可以提升併發編程的效率呢?由於對於英特爾酷睿i七、酷睿、Atom和
NetBurst,以及Core Solo和Pentium M處理器的L一、L2或L3緩存的高速緩存行是64個字節寬,不
支持部分填充緩存行,這意味着,若是隊列的頭節點和尾節點都不足64字節的話,處理器會將
它們都讀到同一個高速緩存行中,在多處理器下每一個處理器都會緩存一樣的頭、尾節點,當一
個處理器試圖修改頭節點時,會將整個緩存行鎖定,那麼在緩存一致性機制的做用下,會致使
其餘處理器不能訪問本身高速緩存中的尾節點,而隊列的入隊和出隊操做則須要不停修改頭
節點和尾節點,因此在多處理器的狀況下將會嚴重影響到隊列的入隊和出隊效率。Doug lea使
用追加到64字節的方式來填滿高速緩衝區的緩存行,避免頭節點和尾節點加載到同一個緩存
行,使頭、尾節點在修改時不會互相鎖定。(鎖定就是鎖定一行64字節,頭尾節點在一行以後,操做頭節點會鎖定這一行,那麼其餘處理器就不能操做尾節點

 

那麼是否是在使用volatile變量時都應該追加到64字節呢?不是的。在兩種場景下不該該
使用這種方式。
--緩存行非64字節寬的處理器。如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個
字節寬。
--共享變量不會被頻繁地寫。由於使用追加字節的方式須要處理器讀取更多的字節到高速
緩衝區,這自己就會帶來必定的性能消耗,若是共享變量不被頻繁寫的話,鎖的概率也很是
,就不必經過追加字節的方式來避免相互鎖定

 

不過這種追加字節的方式在Java 7 可能不生效,由於Java 7變得更加智慧,它會淘汰或
從新排列無用字段,須要使用其餘追加字節的方式。除了volatile,Java併發編程中應用較多的
是synchronized,下面一塊兒來看一下。

由於它會鎖住總線,致使其餘CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內
存。

 


只要兩個指令之間不存在數據依賴,就能夠對這兩個指令亂序。沒必要關心數據依賴的精肯定義,能夠理解爲:只要不影響程序單線程、順序執行的結果,就能夠對兩個指令重排序。

 

public class OutofOrderExecution {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    
    public static void main(String[] args)
        throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                a = 1;
                x = b;
            }
        });
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                b = 1;
                y = a;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(「(」 + x + 「,」 + y + 「)」);
    }
}

public class MutableInteger {
    private int value;
        
    public int get(){
        return value;
    }
        
    public void set(int value){
        this.value = value;
    }
}

class Singleton {
    private static Singleton instance;
        
    private Singleton(){}
        
    public static Singleton getInstance() {
        if (instance == null) { // 這裏存在競態條件
            instance = new Singleton();
        }
        return instance;
    }

競態條件會致使instance引用被屢次賦值,使用戶獲得兩個不一樣的單例。
class Singleton {
    private static Singleton instance;
    
    public int f1 = 1;   // 觸發部分初始化問題
    public int f2 = 2;
        
    private Singleton(){}
    
    public static Singleton getInstance() {
        if (instance == null) { // 當instance不爲null時,可能指向一個「被部分初始化的對象」
            synchronized (Singleton.class) {
                if ( instance == null ) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

 


 

在多線程的環境下,若是某個線程首次讀取共享變量,則首先到主內存中獲取該變量,而後存入工做內存中,之後只須要在工做內存中讀取該變量便可。一樣若是對該變量執行了修改的操做,則先將新值寫入工做內存中,而後再刷新至主內存中。可是何時最新的值會被刷新至主內存中是不太肯定的,這也就解釋了爲何VolatileFoo中的Reader線程始終沒法獲取到init_value最新的變化。
· 使用關鍵字volatile,當一個變量被volatile關鍵字修飾時,對於共享資源的讀操做會直接在主內存中進行(固然也會緩存到工做內存中,當其餘線程對該共享資源進行了修改,則會致使當前線程在工做內存中的共享資源失效,因此必須從主內存中再次獲取),對於共享資源的寫操做固然是先要修改工做內存,可是修改結束後會馬上將其刷新到主內存中。
· 經過synchronized關鍵字可以保證可見性,synchronized關鍵字可以保證同一時刻只有一個線程得到鎖,而後執行同步方法,而且還會確保在鎖釋放以前,會將對變量的修改刷新到主內存當中
· 經過JUC提供的顯式鎖Lock也可以保證可見性,Lock的lock方法可以保證在同一時 刻只有一個線程得到鎖而後執行同步方法,而且會確保在鎖釋放(Lock的unlock方法)以前會將對變量的修改刷新到主內存當中。

CAS其非阻塞性,它對死鎖問題天生免疫,就是不使用鎖沒有鎖的概念。

兩個線程同時使用一個共享變量,會在Cache中緩存該變量,當一個線程修改共享變量時,Cache未能及時將修改的值放回RAM,致使另外一個線程不能讀取修改後的值。
volatile關鍵字的做用:用來保證對變量修改後,能當即寫回主存,從而保證共享變量的修改對全部線程是可見的。

 

 

public class LinkedQueue <E> {
    private static class Node <E> {
        final E item;
        final AtomicReference<Node<E>> next;
        Node(E item, Node<E> next) {
            this.item = item;
            this.next = new AtomicReference<Node<E>>(next);
        }
    }
    private AtomicReference<Node<E>> head
        = new AtomicReference<Node<E>>(new Node<E>(null, null));
    private AtomicReference<Node<E>> tail = head;
    public boolean put(E item) {
        Node<E> newNode = new Node<E>(item, null);
        while (true) {
            Node<E> curTail = tail.get();
            Node<E> residue = curTail.next.get();
            if (curTail == tail.get()) {//暫停了,下次歷來
                if (residue == null) /* A */ {//暫停了,下次歷來
                    if (curTail.next.compareAndSet(null, newNode)) /* C */ {//暫停了,下次歷來
                        tail.compareAndSet(curTail, newNode) /* D */ ;//暫停了,下次歷來
                        return true;
                    }
                } else {
                    tail.compareAndSet(curTail, residue) /* B */;//幫助別人作
                }
            }
        }
    }
}

 

相關文章
相關標籤/搜索