這裏主要描述的線程,工做內存,主存的變量的讀寫關係:java
(1) 操做use以前必須先執行read和load操做。安全
(2) 操做assign以後必須執行store和write操做。多線程
由特性性保證了read、load和use的操做連續性,assign、store和write的操做連續性,從而達到工做內存讀取前必須刷新主存最新值;工做內存寫入後必須同步到主存中。讀取的連續性和寫入的連續性,看上去像線程直接操做了主存。ide
擴展: lock和unlock操做並不直接開放給用戶使用,而是提供給像Synchronize關鍵字指定monitorenter和monitorexit隱式使用。關於Synchronize的監聽器鎖monitor,javac編譯後會在做用的方法先後增長monitorenter和monitorexit指令,詳細的能夠查看Synchronize原理。性能
public class VolatileVisibility {
public static class TestData {
volatile int num = 0;
public void updateNum(){
num = 1;
}
}
public static void main(String[] args) {
final TestData testData = new TestData();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("ChildThread num-->"+testData.num);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
testData.updateNum();
System.out.println("ChildThread update num-->"+testData.num);
}
}).start();
while (testData.num == 0){
}
System.out.println("MainThread num-->"+testData.num);
}
}
複製代碼
(1) TestData
中的num不添加volatile
關鍵字,System.out.println("MainThread num-->"+testData.num);
這一句一直不會執行。表示while中的條件testData.num == 0一直爲0,子線程修改了num對主線程不起做用。優化
(2) TestData
中的num添加volatile
關鍵字,System.out.println("MainThread num-->"+testData.num);
會執行,結果以下。spa
ChildThread num-->0
ChildThread update num-->1
MainThread num-->1
複製代碼
volatile自己並不對數據運算處理維持原子性,強調的是讀寫及時影響主存。線程
volatile修飾num,num++;num = num+1;這種就是非原子性操做。指針
(1) 主存讀取num的值;code
(2) 進行num++運算;
(3) 將num值寫到主存。
像種操做在多線程環境中,use和assign是屢次出現,若是有兩個線程中讀取到主存的num都是2,且同時執行num++,兩個線程的結果都是3,這樣就產生了髒數據,再寫入主存中都是3。核心num++運算並沒保證前後順序執行。爲了保證執行運算的線程順序,能夠選擇Synchronize。
public class ValatileAtomic {
public static class TestData {
volatile int num = 0;
//synchronized
public void updateNum(){
num++;
}
}
public static void main(String[] args) {
final TestData testData = new TestData();
for(int i = 1; i <= 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 1; j <= 1000; j++) {
testData.updateNum();
}
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("最終結果:" + testData.num);
}
}
複製代碼
按咱們的意願10個線程,每一個線程累加線程累加1000,一共是10 * 1000=10000。可是volatile int num = 0;
使用volatile與否都是體現非原子性,運行的結果都比10000小:
最終結果:9701
複製代碼
爲了實現同步操做,在方法updateNum()前添加關鍵字synchronize便可:
最終結果:10000
複製代碼
(1) 指令重排:爲了提升性能,編譯器和和處理器一般會對指令進行指令重排序。
圖中的三個重排位置能夠調換的,根據系統優化須要進行重排。遵循的原則是單線程重排後的執行結果要與順序執行結果相同。(2) 內存屏障指令:volatile在指令之間插入內存屏障,保證按照特定順序執行和某些變量的可見性。
volatile就是經過內存屏障通知cpu和編譯器不作指令重排優化來維持有序性。
(1) synchronize無禁止指令重排。
(2) 一個變量在同一時刻只容許一條線程對其進行lock操做,獲取對象鎖,互斥排他性達到兩個同步塊串行執行。
因爲volatile的非原子性緣由,因此它的線程安全是有條件的:
(1) 運算結果不依賴但前置,或者能保證自由一個單一線程修改變量值。
(2) 變量不須要與其餘的狀態變量共同參與不變的約束。 這兩條件描述出自於《深刻理解java虛擬機》。
public 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;
}
}
複製代碼
按照上邊的寫法已經對new Singleton();這個操做進行了synchronize操做,已經保證了多線程只能串行執行這個實例化代碼。事實上,synchronize保證了線程執行實例化這段代碼是串行的,可是Synchronize並不具有禁止指令重排的特性。
而instance = new Singleton();
主要作了3件事情:
(1) java虛擬機爲對象分配一塊內存x。
(2) 在內存x上爲對象進行初始化 。
(3) 將內存x的地址賦值給instance 變量。
若是編譯器進行重排爲:
(1) java虛擬機爲對象分配一塊內存x。
(2) 將內存x的地址賦值給instance 變量。
(3) 在內存x上爲對象進行初始化 。
第一種狀況,無volatile修飾:此時,有兩個線程執行getInstance()方法,加入線程A進入代碼的註釋中的第②處,並執行到了重排指令的(2),與其同時線程B恰好代碼註釋中的第①處的if判斷。此時,instance有線程A把內存地址x地址賦值給了instance,那麼instance已經不爲空只是沒有初始化完成,線程B就返回了一個沒有完成初始化的instance,最終使用時候會出現空指針的錯誤。
第二種狀況,有volatile修飾:instance由於被volatile的禁止指令重排的特性,那隻會安裝先初始化對象再賦值給instance這樣順序執行,這樣就能保證返回正常的實例化的對象。
1.volatile具備可見性和有序性,不能保證原子性。
2.volatile在特定狀況下線程安全,好比自身不作非原子性運算。
3.synchronize經過獲取對象鎖,保證代碼塊串行執行,無禁止指令重排能力。
4.DCL單例操做須要volatile和synchronize保證線程安全。