volatile
先來說說volatie的作用
- 禁止指令重排
- 保證變量的可見性,但是不能保證互斥性
具體實(shí)現(xiàn)是采用了內(nèi)存屏障
在《并發(fā)編程藝術(shù)》這本書中說到被volatile修飾的變量進(jìn)行寫操作的時(shí)候星虹,會(huì)多出一行l(wèi)ock前綴的指令,觸發(fā)兩件事
- 將當(dāng)前處理器的緩存行數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
- 這個(gè)寫回到內(nèi)存的操作會(huì)使其他CPU里的緩存了該內(nèi)存地址的數(shù)據(jù)無效
對(duì)象大小
我們知道java對(duì)象頭的大小在32系統(tǒng)下面是8B隙弛,但是在64位系統(tǒng)下面就是16B,但是在java8里面笛匙,默認(rèn)開啟了指針壓縮洞豁,所以是12B,但是我們都知道是以一個(gè)字寬為單位的茸歧,所以padding 4B倦炒,我們導(dǎo)入個(gè)小工具
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>4.2.0</version>
</dependency>
然后測(cè)試:
class SharingInt {
volatile int value;
}
System.out.println("object size:"+RamUsageEstimator.sizeOf(new Object()));
System.out.println("sharingInt size:"+RamUsageEstimator.sizeOf(new SharingInt()));
結(jié)果:
object size:16
sharingInt size:16
可以看出沒有實(shí)例域的Object是16B,有一個(gè)int實(shí)例域的SharingInt也是16B软瞎,是因?yàn)槟J(rèn)開啟了指針壓縮逢唤,int在java中是占用4B,所以12B+4B=16B涤浇,這借用一下R大的回復(fù)
偽共享
cpu高速緩存中的最小單位是緩存行鳖藕,它的大小一般為32B,64B,128B,265B,現(xiàn)在電腦最常見的緩存行就64B的只锭。當(dāng)多個(gè)線程訪問修改獨(dú)立的變量的時(shí)候著恩,恰好這些變量?jī)?nèi)存地址很接近,同在一條緩存行上面蜻展,由于MESI協(xié)議的原因喉誊,就會(huì)無意之間影響了性能
我們來看一個(gè)例子
class SharingInt {
volatile int value;
// long p1, p2, p3, p4, p5, p6;
}
public class CacheLine extends Thread {
private final SharingInt[] shares;
private final int index;
public CacheLine(SharingInt[] shares, int index) {
this.shares = shares;
this.index = index;
}
/**
* maven 導(dǎo)入小工具
* <dependency>
* <groupId>org.apache.lucene</groupId>
* <artifactId>lucene-core</artifactId>
* <version>4.2.0</version>
* </dependency>
*
*/
public static void main(String[] args) throws InterruptedException {
// System.out.println(RamUsageEstimator.sizeOf(new SharingInt()));
for (int i = 0; i < 10; i++) {
test();
}
}
private static void test() throws InterruptedException {
//cpu 并行處理
int size = Runtime.getRuntime().availableProcessors();
SharingInt[] shares = new SharingInt[size];
for (int i = 0; i < size; i++) {
shares[i] = new SharingInt();
}
Thread[] threads = new Thread[size];
for (int i = 0; i < size; i++) {
threads[i] = new CacheLine(shares, i);
}
for (Thread t : threads) {
t.start();
}
long start = System.currentTimeMillis();
for (Thread t : threads) {
t.join();
}
long end = System.currentTimeMillis();
System.out.printf("用時(shí): %dms\n", end - start);
}
@Override
public void run() {
for (int i = 0; i < 100000000; i++) {
shares[index].value++;
}
}
}
代碼很簡(jiǎn)單,N(與CPU核心相同)條線程共享同一個(gè)數(shù)組纵顾,讓1~N條線程分別訪問同一個(gè)數(shù)組的不同下標(biāo)伍茄,互不干擾,每個(gè)線程循環(huán)1億次讀寫操作(shares[index].v++)
我的電腦是4核8線程64位的系統(tǒng)施逾,運(yùn)行結(jié)果如下:
用時(shí): 10531ms
用時(shí): 9665ms
用時(shí): 9668ms
用時(shí): 9974ms
用時(shí): 10364ms
用時(shí): 10250ms
用時(shí): 10342ms
用時(shí): 10982ms
用時(shí): 10604ms
用時(shí): 10931ms
然后再去掉SharingInt里面的注釋幻林,再跑一遍
用時(shí): 3735ms
用時(shí): 4082ms
用時(shí): 4007ms
用時(shí): 1376ms
用時(shí): 3860ms
用時(shí): 3685ms
用時(shí): 4366ms
用時(shí): 1341ms
用時(shí): 3039ms
用時(shí): 3777ms
為什么會(huì)有那么大的差距呢贞盯?是因?yàn)閭喂蚕淼木壒十?dāng)?shù)谝粭l線程返回index=0的時(shí)候
- 假設(shè)線程1音念,線程2分別在Core1沪饺,Core2中獲取到時(shí)間令牌,然后都會(huì)加載Cache Line 1闷愤,這時(shí)候Cache Line 1的狀態(tài)是S(共享)
- 然后可能線程1先修改了index=0的SharingInt.value整葡,然后Cache Line 1 從 S變?yōu)?strong>M(修改),然后根據(jù)volatile的語義讥脐,然后立馬把Cache Line 寫回到主存遭居,然后Cache Line 1 的狀態(tài)置從M變?yōu)?strong>I(無效)
- 然后等到Core2 需要修改index=1的SharingInt.value時(shí),發(fā)現(xiàn)Cache Line 1 的狀態(tài)為I(無效)旬渠,然后直又從主存讀取Cache Line 1進(jìn)來俱萍,然后把狀態(tài)變?yōu)?strong>E獨(dú)享,然后修改value之后告丢,又將Cache Line 刷新回主存枪蘑。
以上就是MESI緩存一致性協(xié)議的工作過程,可以看出一條一樣數(shù)據(jù)被多讀進(jìn)一次CPU 的cache岖免,所以這個(gè)操作就消耗了時(shí)間
避免偽共享
避免偽共享的兩種方式:
1.增大對(duì)象的空間岳颇,使得需要訪問的數(shù)據(jù)不在同一個(gè)Cache Line上面,典型的空間換時(shí)間的方法
- 在每個(gè)線程添加本地副本颅湘,等待完全修改完成后再寫回主存
padding:
修改SharingInt
class SharingInt {
volatile int value;
long p1, p2, p3, p4, p5, p6;
}
System.out.println("sharingInt size:"+RamUsageEstimator.sizeOf(new SharingInt()));
sharingInt size:64
這樣一個(gè)SharingInt對(duì)象就填充了一個(gè)緩存行了话侧,在java中一個(gè)long就是8B,加多6個(gè)剛剛好64B闯参,這樣子各個(gè)線程對(duì)相對(duì)應(yīng)的對(duì)象修改就不會(huì)在不同的緩存行
上面追加之后的結(jié)果之后雖然快了很多瞻鹏,但是你會(huì)發(fā)現(xiàn)有一些1秒多有一些需要3秒多甚至4秒。這就是這種方式的不好之處鹿寨,因?yàn)閭€(gè)人的操作系統(tǒng)或者CPU架構(gòu)都可能不一樣新博,
java7會(huì)優(yōu)化這種字節(jié)追加方式而導(dǎo)致失效,但是查看java8編譯的字節(jié)碼來看释移,并沒有優(yōu)化掉,但是沒有辦法穩(wěn)定下來
以繼承的方式避免優(yōu)化
我們修改一下SharingInt
class Temp{
long p1,p2,p3,p4,p5,p6;
}
class SharingInt extends Temp{
volatile int value;
}
System.out.println("sharingInt size:"+RamUsageEstimator.sizeOf(new SharingInt()));
sharingInt size:72
有人可能會(huì)問為什么多了8B叭披,因?yàn)槭抢^承關(guān)系,子類會(huì)多一個(gè)Reference類型玩讳,Reference類型在java中占4B涩蜘,然后padding 4B就剛剛好72B
運(yùn)行結(jié)果
用時(shí): 1623ms
用時(shí): 1305ms
用時(shí): 1295ms
用時(shí): 1307ms
用時(shí): 1279ms
用時(shí): 1286ms
用時(shí): 1277ms
用時(shí): 1269ms
用時(shí): 1279ms
用時(shí): 1312ms
雖然穩(wěn)定了優(yōu)化但是這樣某一天java又進(jìn)行了一系列的優(yōu)化也許也不行了,但是在在java8給出了官方的實(shí)現(xiàn)
@Contended
在2012年openjdk的JEP-142說到使用這個(gè)注解可以自動(dòng)追加合適的大小padding
這個(gè)注解需要是用在用戶代碼上面(非bootstrap class loader或者extension class loader所加載的類)熏纯,并且需要添加-XX:-RestrictContended啟動(dòng)參數(shù)
我們修改SharingInt
class SharingInt {
@Contended
volatile int value;
}
System.out.println("sharingInt size:"+RamUsageEstimator.sizeOf(new SharingInt()));
sharingInt size:144
我們看到SharingInt被追加128B的padding同诫,在JEP-142中提及
Note that we use 128 bytes, twice the cache line size on most hardware
to adjust for adjacent sector prefetchers extending the false sharing
collisions to two cache lines.
padding的大小定義為目前大多數(shù)CPU的Cache Line 大小的2倍,就是128B
分組功能:
There are cases where you want to separate the group of fields that
are experiencing contention with everything else but not pairwise. This
is the usual thing for some of the code updating two fields at once.
While marking both with @Contended would be sufficient, we can optimize
the memory footprint by not applying padding between them. In order to
demarcate these groups, we have the parameter in the annotation
describing the equivalence class for contention group.
意思就是如果兩個(gè)字段a樟澜,b都被一個(gè)CPU修改误窖,雖然各自追加padding就足夠了叮盘,但是jvm可以將a,b字段優(yōu)化在一個(gè)Cache Line上面
我們看一個(gè)例子:
class VolatileLong {
@Contended("1")
public volatile long value1 = 0L;
@Contended("1")
public volatile long value2 = 0L;
@Contended("2")
public volatile long value3 = 0L;
@Contended("2")
public volatile long value4 = 0L;
}
public final class ContendedTest implements Runnable {
private final VolatileLong volatileLong;
private final int id;
public ContendedTest(int id,VolatileLong volatileLong) {
this.id = id;
this.volatileLong = volatileLong;
}
//-XX:-RestrictContended
public static void main(final String[] args) throws Exception {
runTest();
}
private static void runTest() throws InterruptedException {
VolatileLong volatileLong = new VolatileLong();
Thread t0 = new Thread(new ContendedTest(1,volatileLong));
Thread t1 = new Thread(new ContendedTest(2,volatileLong));
final long start = System.currentTimeMillis();
t0.start();
t1.start();
t0.join();
t1.join();
System.out.println("用時(shí):" + (System.currentTimeMillis() - start)+"ms");
}
@Override
public void run() {
long i = 500000000;
if (1 == id) {
while (0 != i--) {
volatileLong.value1 = i;
volatileLong.value2 = i;
}
} else if (2 == id) {
while (0 != i--) {
volatileLong.value3 = i;
volatileLong.value4 = i;
}
}
}
}
運(yùn)行結(jié)果:用時(shí):6151ms
代碼很簡(jiǎn)單,兩個(gè)線程分別對(duì)兩個(gè)long變量賦值霹俺,重復(fù)5億次使用了6s的時(shí)間
我們將run()方法替換如下再跑一遍
@Override
public void run() {
long i = 500000000;
if (1 == id) {
while (0 != i--) {
volatileLong.value1 = i;
volatileLong.value3 = i;
}
} else if (2 == id) {
while (0 != i--) {
volatileLong.value2 = i;
volatileLong.value4 = i;
}
}
}
運(yùn)行結(jié)果:用時(shí):23963ms
那是使用了@contended
注解分組
- value1,value2被分配到了一條Cache Line
- value3 value4被分配到了一條Cache Line
兩條線程相互修改對(duì)方的Cache Line柔吼,又要從主存里面重新讀取最新的數(shù)據(jù),所以這件花費(fèi)了大量的時(shí)間
本地變量副本
在JMM(java Memory Model)中丙唧,每一個(gè)線程都會(huì)有一個(gè)線程副本愈魏,每一次修改完之后不會(huì)立馬刷新回主存,而是等處理完之后才刷新會(huì)主存
我們改一下上面的VolatileLong
class VolatileLong {
@Contended("1")
public long value1 = 0L;
@Contended("1")
public long value2 = 0L;
@Contended("2")
public long value3 = 0L;
@Contended("2")
public long value4 = 0L;
}
我們分別使用兩種run方法去執(zhí)行想际,兩個(gè)方法的耗時(shí)
第一中run方法
用時(shí):398ms
第二種run方法
用時(shí):2871ms
雖然有差距培漏,但是也沒有之前那么嚴(yán)重了,所以使用volatile需要謹(jǐn)慎