Java中JMM與volatile關(guān)鍵字的學(xué)習(xí)
JMM
JMM是指Java內(nèi)存模型,不是Java內(nèi)存布局,不是所謂的棧、堆、方法區(qū)。
每個Java線程都有自己的工作內(nèi)存。操作數(shù)據(jù),首先從主內(nèi)存中讀,得到一份拷貝,操作完畢后再寫回到主內(nèi)存。
JMM可能帶來可見性、原子性和有序性問題。
1.可見性:指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。顯然,對于串行程序來說,可見性問題 是不存在。因為你在任何一個操作步驟中修改某個變量,那么在后續(xù)的步驟中,讀取這個變量的值,一定是修改后的新值。但是這個問題在并行程序中就不見得了。如果一個線程修改了某一個全局變量,那么其他線程未必可以馬上知道這個改動。
2.原子性:指一個操作是不可中斷的,即使是多個線程一起執(zhí)行的時候,一個線程操作一旦開始,就不會被其他線程干擾比如,對于一個靜態(tài)全局變量int i,兩個線程同時對它賦值,線程A 給他賦值 1,線程 B 給它賦值為 -1,。那么不管這兩個線程以何種方式,何種步調(diào)工作,i的值要么是1,要么是-1,線程A和線程B之間是沒有干擾的。這就是原子性的一個特點,不可被中斷。
3.有序性:對于一個線程的執(zhí)行代碼而言,我們總是習(xí)慣地認為代碼的執(zhí)行時從先往后,依次執(zhí)行的。這樣的理解也不能說完全錯誤,因為就一個線程而言,確實會這樣。但是在并發(fā)時,程序的執(zhí)行可能就會出現(xiàn)亂序。給人直觀的感覺就是:寫在前面的代碼,會在后面執(zhí)行。有序性問題的原因是因為程序在執(zhí)行時,可能會進行指令重排,重排后的指令與原指令的順序未必一致。
volatile關(guān)鍵字
volatile關(guān)鍵字是Java提供的一種輕量級同步機制。它能夠保證可見性和有序性,但是不能保證原子性。
可見性與原子性測試
class MyData{ int number=0; //volatile int number=0; AtomicInteger atomicInteger=new AtomicInteger(); public void setTo60(){ this.number=60; } //此時number前面已經(jīng)加了volatile,但是不保證原子性 public void addPlusPlus(){ number++; } public void addAtomic(){ atomicInteger.getAndIncrement(); } } //volatile可以保證可見性,及時通知其它線程主物理內(nèi)存的值已被修改 private static void volatileVisibilityDemo() { System.out.println("可見性測試"); MyData myData=new MyData();//資源類 //啟動一個線程操作共享數(shù)據(jù) new Thread(()->{ System.out.println(Thread.currentThread().getName()+"\t come in"); try {TimeUnit.SECONDS.sleep(3);myData.setTo60(); System.out.println(Thread.currentThread().getName()+"\t update number value: "+myData.number);}catch (InterruptedException e){e.printStackTrace();} },"AAA").start(); while (myData.number==0){ //main線程持有共享數(shù)據(jù)的拷貝,一直為0 } System.out.println(Thread.currentThread().getName()+"\t mission is over. main get number value: "+myData.number); }
可見性:
MyData類是資源類,一開始number變量沒有用volatile修飾,所以程序運行的結(jié)果是:
可見性測試
AAA come in
AAA update number value: 60
雖然"AAA"線程把number修改成了60,但是main線程持有的仍然是最開始的0,所以一直循環(huán),程序不會結(jié)束。
如果對number添加了volatile修飾,運行結(jié)果是:
AAA come in
AAA update number value: 60
main mission is over. main get number value: 60
可見某個線程對number的修改,會立刻反映到主內(nèi)存上。
原子性:
volatile并不能保證操作的原子性。這是因為,比如一條number++的操作,底層會形成3條指令。
getfield //讀 iconst_1 //++常量1 iadd //加操作 putfield //寫操作
假設(shè)有3個線程,分別執(zhí)行number++,都先從主內(nèi)存中拿到最開始的值,number=0,然后三個線程分別進行操作。假設(shè)線程A執(zhí)行完畢,number=1,也立刻通知到了其它線程,但是此時線程B、C已經(jīng)拿到了number=0,所以結(jié)果就是寫覆蓋,線程B、C將number變成1。
解決的辦法就是:
- 對
addPlusPlus()
方法加鎖。 - 使用
java.util.concurrent.AtomicInteger
類。
private static void atomicDemo() { System.out.println("原子性測試"); MyData myData=new MyData(); for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 0; j <1000 ; j++) { myData.addPlusPlus(); myData.addAtomic(); } },String.valueOf(i)).start(); } while (Thread.activeCount()>2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t int type finally number value: "+myData.number); System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value: "+myData.atomicInteger); }
結(jié)果:可見,由于volatile
不能保證原子性,出現(xiàn)了線程重復(fù)寫的問題,最終結(jié)果比20000小。而AtomicInteger
可以保證原子性。
原子性測試 main int type finally number value: 17542 main AtomicInteger type finally number value: 20000
有序性:
volatile可以保證有序性,也就是防止指令重排序。所謂指令重排序,就是出于優(yōu)化考慮,CPU執(zhí)行指令的順序跟程序員自己編寫的順序不一致。就好比一份試卷,題號是老師規(guī)定的(代碼是程序員規(guī)定的),但是考生(CPU)可以先做選擇題,也可以先做填空題。
但是有時候這種情況就會出現(xiàn)問題:
int x = 11; //語句1 int y = 12; //語句2 x = x + 5; //語句3 y = x * x; //語句4
以上例子,可能出現(xiàn)的執(zhí)行順序有1234、2134、1342,這三個都沒有問題,最終結(jié)果都是x = 16,y=256。但是如果是4開頭,就有問題了,y=0。這個時候就不需要指令重排序。
哪些地方用到過volatile?
單例模式的安全問題
常見的DCL(Double Check Lock)模式雖然加了同步,但是在多線程下依然會有線程安全問題。
public class SingletonDemo { private static SingletonDemo singletonDemo=null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName()+"\t 我是構(gòu)造方法"); } //DCL模式 Double Check Lock 雙端檢索機制:在加鎖前后都進行判斷 public static SingletonDemo getInstance(){ if (singletonDemo==null){ synchronized (SingletonDemo.class){ if (singletonDemo==null){ singletonDemo=new SingletonDemo(); } } } return singletonDemo; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ SingletonDemo.getInstance(); },String.valueOf(i+1)).start(); } } }
這個漏洞比較tricky,很難捕捉,但是是存在的。instance=new SingletonDemo();可以大致分為三步:
memory = allocate(); //1.分配內(nèi)存 instance(memory); //2.初始化對象 instance = memory; //3.設(shè)置引用地址
由于Java編譯器允許處理器亂序執(zhí)行,以及JDK1.5之前JMM(Java Memory Medel,即Java內(nèi)存模型)中Cache、寄存器到主內(nèi)存回寫順序的規(guī)定,上面的第二點和第三點的順序是無法保證的,也就是說,執(zhí)行順序可能是1-2-3也可能是1-3-2,如果是后者,并且在3執(zhí)行完畢、2未執(zhí)行之前,被切換到線程B上,這時候instance因為已經(jīng)在線程A內(nèi)執(zhí)行過了第三點,instance已經(jīng)是非空了,所以線程B直接拿走instance,然后使用,然后順理成章地報錯,而且這種難以跟蹤難以重現(xiàn)的錯誤很可能會隱藏很久。
解決的方法就是對singletondemo
對象添加上volatile
關(guān)鍵字,禁止指令重排。
你知道CAS嗎?
CAS是指Compare And Swap,比較并交換,是一種很重要的同步思想。如果主內(nèi)存的值跟期望值一樣,那么就進行修改,否則一直重試,直到一致為止。
public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger=new AtomicInteger(5); System.out.println(atomicInteger.compareAndSet(5, 2021)+"\t current data : "+ atomicInteger.get()); //修改失敗 System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data : "+ atomicInteger.get()); } }
第一次修改,期望值為5,主內(nèi)存也為5,主內(nèi)存的值修改成功,為2021;第二次修改,期望值為5,主內(nèi)存實際值為2021,修改失敗。
CAS底層原理
public final int getAndIncrement(){ return unsafe.getAndAddInt(this,valueOffset,1); }
查看AtomicInteger.getAndIncrement()
方法,發(fā)現(xiàn)其沒有加synchronized
也實現(xiàn)了同步。這是為什么?
AtomicInteger
內(nèi)部維護了volatile int value
和private static final Unsafe unsafe
兩個比較重要的參數(shù)。
AtomicInteger.getAndIncrement()
調(diào)用了Unsafe.getAndAddInt()
方法。Unsafe類的大部分方法都是native的,用來像C語言一樣從底層操作內(nèi)存。
public final int getAnddAddInt(Object var1,long var2,int var4){ int var5; do{ var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
這個方法的var1和var2,就是根據(jù)對象和偏移量得到在主內(nèi)存的快照值var5。然后compareAndSwapInt
方法通過var1和var2得到當前主內(nèi)存的實際值。如果這個實際值跟快照值相等,那么就更新主內(nèi)存的值為var5+var4。如果不等,那么就一直循環(huán),一直獲取快照,一直對比,直到實際值和快照值相等為止。
比如有A、B兩個線程,一開始都從主內(nèi)存中拷貝了原值為3,A線程執(zhí)行到var5=this.getIntVolatile
,即var5=3。此時A線程掛起,B修改原值為4,B線程執(zhí)行完畢,由于加了volatile,所以這個修改是立即可見的。A線程被喚醒,執(zhí)行this.compareAndSwapInt()
方法,發(fā)現(xiàn)這個時候主內(nèi)存的值不等于快照值3,所以繼續(xù)循環(huán),重新從主內(nèi)存獲取。
CAS缺點
CAS實際上是一種自旋鎖
- 一直循環(huán)等待,開銷比較大。
- 只能保證一個變量的原子操作,多個變量依然要加鎖。
- 引出ABA問題。
ABA問題
所謂的ABA問題,就是比較并交換的循環(huán),存在一個時間差,而這個時間差可能帶來意想不到的問題。比如線程T1將一個值從A改為B,然后又從B改為A。當線程T2訪問時,看到的就是A,但是卻不知道這個A其實發(fā)生了更改。盡管線程T2 CAS操作成功,但是不代表就沒有問題。
有的需求,比如CAS,只注重頭尾(只看期望值和實際值),只要首尾一致就接受。但是有的需求,還看重過程,中間不能發(fā)生任何修改,這就引出了AtomicReference:原子引用。
AtomicReference
AtomicInteger
對整數(shù)進行原子操作,但是如果對象是一個POJO呢?我們這時就可以使用AtomicReference
來包裝這個POJO,使其操作原子化。
User user1 = new User("Jack",25); User user2 = new User("Lucy",21); AtomicReference<User> atomicReference = new AtomicReference<>(); atomicReference.set(user1); System.out.println(atomicReference.compareAndSet(user1,user2)); // true System.out.println(atomicReference.compareAndSet(user1,user2)); //false
AtomicStampedReference
和ABA
問題的解決
使用AtomicStampedReference類可以解決ABA問題。這個類維護了一個版本號Stamp,在進行CAS操作的時候,不僅要比較當前值,還要比較版本號。只有兩者都相等,才能執(zhí)行更新操作。
AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);
使用實例:
package thread; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo { static AtomicReference<Integer> atomicReference = new AtomicReference<>(100); static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); public static void main(String[] args) { System.out.println("======ABA問題的產(chǎn)生======"); new Thread(() -> { atomicReference.compareAndSet(100, 101); atomicReference.compareAndSet(101, 100); }, "t1").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get().toString()); }, "t2").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("======ABA問題的解決======"); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "\t第一次版本號: " + stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100,101, atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName() + "\t第二次版本號: " + atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101,100, atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName() + "\t第三次版本號: " + atomicStampedReference.getStamp()); }, "t3").start(); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "\t第一次版本號: " + stamp); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } boolean result=atomicStampedReference.compareAndSet(100,2019, stamp,stamp+1); System.out.println(Thread.currentThread().getName()+"\t修改成功與否:"+result+" 當前最新版本號"+atomicStampedReference.getStamp()); System.out.println(Thread.currentThread().getName()+"\t當前實際值:"+atomicStampedReference.getReference()); }, "t4").start(); } }
總結(jié)
總結(jié)來源于GitHub,內(nèi)部帶有源碼和用例圖。
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
詳解Java 包掃描實現(xiàn)和應(yīng)用(Jar篇)
這篇文章主要介紹了詳解Java 包掃描實現(xiàn)和應(yīng)用(Jar篇),本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07SpringBoot如何導(dǎo)出Jar包并測試(使用IDEA)
這篇文章主要介紹了SpringBoot如何導(dǎo)出Jar包并測試(使用IDEA),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07SpringBoot 如何使用Dataway配置數(shù)據(jù)查詢接口
這篇文章主要介紹了SpringBoot 如何使用Dataway配置數(shù)據(jù)查詢接口,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11三分鐘讀懂mybatis中resultMap和resultType區(qū)別
這篇文章主要給大家介紹了mybatis中resultMap和resultType區(qū)別的相關(guān)資料,resultType和resultMap都是mybatis進行數(shù)據(jù)庫連接操作處理返回結(jié)果的,需要的朋友可以參考下2023-07-07SpringCloud 服務(wù)注冊和消費實現(xiàn)過程
這篇文章主要介紹了SpringCloud 服務(wù)注冊和消費實現(xiàn)過程,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07頁面的緩存與不緩存設(shè)置及html頁面中meta的作用
這篇文章主要介紹了頁面的緩存與不緩存設(shè)置及html頁面中meta的作用的相關(guān)資料,需要的朋友可以參考下2016-05-05淺析JAVA常用JDBC連接數(shù)據(jù)庫的方法總結(jié)
本篇文章是對在JAVA中常用JDBC連接數(shù)據(jù)庫的方法進行了詳細的總結(jié)分析,需要的朋友參考下2013-07-07