Java volatile的效能分析
volatile通過記憶體屏障來實現禁止重排序,通過Lock執行來實現執行緒可見性,如果我們的程式中需要讓其他執行緒及時的對我們的更改可見可以使用volatile關鍵字來修飾,比如AQS中的state
所以在一個執行緒寫,多個執行緒讀的情況下,或者是對volatile修飾的變數進行原子操作時,是可以實現共享變數的同步的,但是i++ 不行,因為i++ 又三個操作組成,先讀出值,然後再對值進行+1 ,接著講結果寫入,這個過程,如果中間有其他執行緒對該變數進行了修改,那麼這個值就無法得到正確的結果。
今天我們討論的重點不是他的功能,而是他的效能問題,首先我們可以看下我們對非volatile變數進行操作,迴圈+1,多個執行緒操作多個變數(這裡不存在併發,至於為什麼要多個執行緒跑,後面就知道了)
首先定義一個Data,內容是四個long型別的變數,我們將會使用四個執行緒分別對他們進行遞增計算操作:
class Data {
public long value1 ;
public long value2;
public long value3;
public long value4;
}
執行類:
public class SyncTest extends Thread{ public static void main(String args[]) throws InterruptedException{ Data data = new Data(); ExecutorService es = Executors.newFixedThreadPool(4); long start = System.currentTimeMillis(); int loopcont = 1000000000; Thread t[] = new Thread[4]; t[0] = new Thread(()-> { for(int i=0;i<loopcont;i++){ data.value1 = data.value1+i; } } ); t[1] = new Thread( () -> { for(int i=0;i<loopcont;i++){ data.value2 = data.value2+i; } } ); t[2] = new Thread( () -> { for(int i=0;i<loopcont;i++){ data.value3 = data.value3+i; } } ); t[3] = new Thread( () -> { for(int i=0;i<loopcont;i++){ data.value4 = data.value4+i; } } ); for(Thread item:t){ es.submit(item); } for(Thread item:t){ item.join(); } es.shutdown(); es.awaitTermination(9999999, TimeUnit.SECONDS); long end = System.currentTimeMillis(); System.out.println(end-start); } }
這樣的結果是:608ms
接著我們用volatile修飾long:
class Data {
public volatile long value1 ;
public volatile long value2;
public volatile long value3;
public volatile long value4;
}
執行結果為:66274
可以看出是100倍左右,使用volatile的效能為什麼會這麼差呢,原因是因為,因為volatile的讀和寫都是要經過主存的,讀會廢棄快取記憶體的地址,從快取讀,寫也會及時重新整理到主存
那麼我們用一個執行緒操作一個變數試試呢:結果是:5362
是要好很多,為什麼多執行緒情況下差距這麼大呢,我們並沒有進行併發操作,並沒有鎖,那是因為發生了偽共享,CPU的快取記憶體的最小單位是快取行,一般是64 byte,這個CPU核心私有的,當我們的cpu核心1 跑執行緒0 , 核心2跑執行緒1的時候,因為區域性性原理,core1的L1快取將value1載入到快取,也會將後面的幾個一併載入進來,core2也一樣,也就是說,core1和core2的快取差不多都把四個值儲存了,而快取行中如果一個值發生變化,cpu會吧整個快取行重新載入,那麼可以理解下,因為記憶體的一致性,就會導致各個核心不停的從主存載入和重新整理,這就導致了效能的問題。
怎麼解決呢:
1.將值拷貝至執行緒內部操作,完成後進行賦值操作,也就是Data中的值依然使用volatile修飾,執行緒的執行邏輯改為:
t[0] = new Thread(()-> {
long value = data.value1 ;
for(int i=0;i<loopcont;i++){
value ++ ;
}
data.value1 = value ;
} );
t[1] = new Thread( () -> {
long value = data.value1 ;
for(int i=0;i<loopcont;i++){
value++;
}
data.value2 = value ;
} );
t[2] = new Thread( () -> {
long value = data.value1 ;
for(int i=0;i<loopcont;i++){
value ++;
}
data.value3 = value ;
} );
t[3] = new Thread( () -> {
long value = data.value1 ;
for(int i=0;i<loopcont;i++){
value ++;
}
data.value4 = value ;
} );
這個結果是多少呢:76ms,可以看到這個比不用volatile修飾還要快很多,那是因為執行緒私有的可以直接線上程內部棧記憶體操作,時間就是cpu消耗的時間,並不會發生記憶體耗時
2使用快取行填充
這裡我們把Data裡面的long修飾一下:
public class VolatileLongPadding {
public volatile long p1, p2, p3, p4, p5, p6; // 註釋
}
package com.demo.rsa;
public class VolatileLong extends VolatileLongPadding {
public volatile long value = 0L;
}
class Data {
public VolatileLong value1 = new VolatileLong();
public VolatileLong value2= new VolatileLong();
public VolatileLong value3= new VolatileLong();
public VolatileLong value4= new VolatileLong();
}
這裡的VolatileLong 通過volatile修飾,並填充了6個無用的long佔空間,加上物件頭,剛好64位元組
邏輯不變,依然是執行緒直接操作value,而不是拷貝到內部:
Thread t[] = new Thread[4];
t[0] = new Thread(()-> {
for(int i=0;i<loopcont;i++){
data.value1.value = data.value1.value+i;
}
} );
t[1] = new Thread( () -> {
for(int i=0;i<loopcont;i++){
data.value2.value = data.value2.value+i;
}
} );
t[2] = new Thread( () -> {
for(int i=0;i<loopcont;i++){
data.value3.value = data.value3.value+i;
}
} );
t[3] = new Thread( () -> {
for(int i=0;i<loopcont;i++){
data.value4.value = data.value4.value+i;
}
} );
這個結果是:44ms,比線上程內部操作還要快。