volatile概念
是java虛擬機提供的輕量級的同步機制
特性
保證可見性 |
說到volatile的可見性就要先說說JMM模型
JMM內存模型
JMM(Java內存模型)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程序中的各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。
JMM關於同步的規定:
- 線程解鎖前,必須把共享變量的值刷回主內存
- 線程加鎖前,必須讀內存的最新值到自己的工作內存
- 加鎖解鎖是同一把鎖
JMM特性
- 可見性
- 原子性
- 有序性
驗證volatile可見性
資源類
class MyData{
int number=0;
public void addTo60(){
this.number=60;
}
}
main方法
public static void main(String[] args){
MyData myData=new MyData();
new Thread(()-> {
System.out.println(Thread.currentThread().getName()+"\t come in");
// 暫停線程
try { TimeUnit.SECONDS.sleep(3);}catch (InterruptedException e){e.printStackTrace();}
myData.addTo60();
System.out.println((Thread.currentThread().getName()+"\t updated number value:" +myData.number));
},"A").start();
// 第2個線程就是我們的main線程
while(myData.number==0){
}
System.out.println(myData.number);
System.out.println(Thread.currentThread().getName()+"\t mission is over main get number:"+myData.number);
}
執行結果
代碼停留在 while(myData.number==0){ },一直沒有動
分析:
這是因爲,在A線程中雖然修改了number=60,但是由於主內存修改後沒有辦法通知線程main,所以number此時仍爲0
優化
結果
分析:
明顯,main線程收到了A線程修改變量的通知,結果被打印出來
不保證原子性 |
原子性:不可分割,完整性,也即某個線程正在做某個具體業務時,中間不可以被加塞或者被分隔。需要整體完整,要麼同時成功,要麼同時失敗。
驗證volatile不保證原子性
資源類
class MyData{
volatile int number=0;
public void addTo60(){
this.number=60;
}
public void addPlus(){
this.number++;
}
}
main
public static void main(String[] args){
MyData myData=new MyData();
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
for (int j=1;j<=1000;j++){
myData.addPlus();
}
},String.valueOf(i)).start();
}
// 需要等待20個線程全部計算完成,再用main線程取得最後結果值
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t final number value"+myData.number);
}
執行結果
分析:
可以看出,外循環20次X內循環1000次調用add方法,結果應該是20000,但是總是加不夠,這就是因爲volatile不保證原子性
看下圖
某一時刻,A B C線程同時訪問主內存,然後都讀取了主內存中number變量的值0,當A將主內存中的0拷貝到工作內存,並加加的時候,此時由於JMM的特性,會將工作內存的值刷回主內存,但是如果由於一些原因A線程掛起,此時B線程將1刷回主內存,並且由於變量number由volatile修飾,具備可見性,會通知各線程,此時A線程不再掛起,由於線程很快,在B的修改還沒有通知到的時候,A將B修改的值再次修改爲1,此時A本來應該修改爲2,就發生了修改丟失的情況
這就是volatile的不保證原子性,那麼如何保證原子性呢?
解決不保證原子性方案
java.util.concurrent.atomic
資源類
lass MyData{
volatile int number=0;
public void addTo60(){
this.number=60;
}
public void addPlus(){
this.number++;
}
AtomicInteger atomicInteger=new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
main
public static void main(String[] args){
MyData myData=new MyData();
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
for (int j=1;j<=1000;j++){
myData.addPlus();
myData.addMyAtomic();
}
},String.valueOf(i)).start();
}
// 需要等待20個線程全部計算完成,再用main線程取得最後結果值
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t final number value"+myData.number);
System.out.println(Thread.currentThread().getName()+"\t atomicInteger value"+myData.atomicInteger);
}
執行結果
結果就保證原子性了
禁止指令重排 |
爲了優化程序性能,編譯器和處理器會對java編譯後的字節碼和機器指令進行重排序,重排序分爲3種類型
- 編譯器優化的重排序,編譯器在不改變單線程語義的前提下,可以重新安排語句的執行順序
- 指令級並行的重排序
- 內存系統的重排序
從源碼到最終執行的指令序列的示意圖