一、前言
最近去成都玩了一圈,整體感覺還不錯,辭職以后工作找的也很順利,隨着年齡增加自己也考慮定居和個人長期發展的問題,反正亂七八糟的事,總之需要好好屢屢思路,不能那么着急下定論,當然我對下份工作也是有所期望的,不扯了開始我們今天主題吧。
二、Java的內存模型
Java內存模型規定所有的變量都存在主內存當中,每條線程都有自己的工作內存,線程的工作內存保存了被該線程使用的變量的主內存副本拷貝,線程對變量的所有操作都在內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間無法直接訪問對工作內存中的變量,線程之間變量值的傳遞均需要通過主內存來完成。----來自深入理解Java虛擬機
這里注意下,Java的內存模型(JMM)和Java的內存區域的差別,不要混淆這兩者之間的概念,JMM主要是圍繞在程序中各個變量在共享數據區域和私有數據區域的訪問方式。JMM與Java內存區域唯一相似點,都存在共享數據區域和私有數據區域,在JMM中主內存屬於共享數據區域,從某個程度上講應該包括了堆和方法區,而工作內存數據線程私有數據區域,從某個程度上講則應該包括程序計數器、虛擬機棧以及本地方法棧。或許在某些地方,我們可能會看見主內存被描述為堆內存,工作內存被稱為線程棧,實際上他們表達的都是同一個含義。
接下來我們來看下主內存和工作內存之間的交互問題:
1.lock操作,鎖定主內存變量,標識為當前線程獨占狀態;
2.read操作,將鎖定的主內存變量讀取到工作內存當中;
3.load操作,將read操作的主線程變量放入到工作變量的副本當中;
4.use操作,將工作內存的變量傳遞給執行引擎,當虛擬機調用到該變量的時候執行該變量;
5.assign操作,當虛擬機對工作內存的變量的值進行賦值操作的時候,將值賦值給工作內存的變量;
6.store操作,將工作內存的變量傳遞給主內存變量;
7.write操作,將store操作的工作變量寫入工作變量;
8.unlock操作,將鎖定的主內存中的變量鎖釋放,等待其他線程鎖定;
明白這些我們再來探究下JMM如何處理原子性、可見性和有序性的問題:
1.原子性
Java內存模型只保證了基本讀取和賦值是原子性操作,如果要實現更大范圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。
2.可見性
由於JMM結構原因,當多線程訪問的時候不能及時將變量的值更新到主內存當中,這個時候就能出現數據不一致的問題,Java提供了volatile關鍵字來保證可見性,另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。
3.有序性
在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程並發執行的正確性。
JMM中有序性可以通過volatile實現,另外通過synchronized和Lock也能夠保證有序性,這個和保證可見性的原理一樣;另外Java語言有一個“先行發生(happens-before)”的原則,如果兩個操作的執行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
下面就來具體介紹下happens-before原則(先行發生原則):
1.程序次序規則:一個線程內書寫在前的代碼先執行,寫在后面的代碼后執行;
2.鎖定規則:一個unLock操作先行發生於lock操作;
3.volatile規則:對一個變量的寫操作先行發生於后面對這個變量的讀操作;
4.線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作
5.線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
6.線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
7.傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
8.對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始
三、volatile
上面聊了很多,接下來我們看下volatile,
volatile作用:
1.保證變量的可見性;

public class VolatileVisibility { public static volatile int i =0; public static void increase(){ i++; } }
針對於可見性我們分析下上面的代碼,i被volatile修飾的情況下,i的任何改變都會反應到其他線程當中,但是當多線程同時調用increase()的方法時,會出現線程安全的問題,i++不是原子操作,該操作先讀后寫,分2步完成,如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那么第二個線程就會與第一個線程一起看到同一個值,並執行相同值的加1操作,這也就造成了線程安全失敗;因此對於increase方法必須使用synchronized修飾,以便保證線程安全。
接下來我們再看一個例子:

public class VolatileDemo { volatile boolean close; public void close(){ close=true; } public void doWork(){ while (!close){ System.out.println("work..."); } } }
被volatile修飾的close字段,字段可以立即可見,保證當多個線程同時訪問實例的時候,一個線程對close進行更新另外一個線程可立即獲取到該字段變化以后的值;當寫一個volatile變量時,JMM會把該線程對應的工作內存中的共享變量值刷新到主內存中,當讀取一個volatile變量時,JMM會把該線程對應的工作內存置為無效,那么該線程將只能從主內存中重新讀取共享變量。
對比下得出一個結論:volatile並不能保證原子性,只能保證可見性;
2.防止指令重排序;
明白這個要我們需要知道一個概念:內存屏障(Memory Barrier)是一個CPU指令,它的作用有兩個,一是保證特定操作的執行順序,二是保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)。由於編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內存屏障禁止在內存屏障前后的指令執行重排序優化。Memory Barrier的另外一個作用是強制刷出各種CPU的緩存數據,因此任何CPU上的線程都能讀取到這些數據的最新版本。
接下來我們看下我們最常見的單例模式:

public class SingletonDemo { private volatile static SingletonDemo instance; private SingletonDemo(){ System.out.println("Singleton has loaded"); } public static SingletonDemo getInstance(){ if(instance==null){ synchronized (SingletonDemo.class){ if(instance==null){ instance=new SingletonDemo(); } } } return instance; } }
這里我們思考下沒有volatile的時候出現的問題:當然在單線程的情況下是不會出現問題,多線程下面會出現問題,首先這里我們要聲明下volatile在這個地方只是防止指令重排,可見性是由鎖實現的,接下來我們在分析為什么多線程會出現問題?
正常情況下初始化一個對象的過程如下:
1.分配內存空間;
2.初始化對象;
3.將內存空間的地址賦值給對象的引用;
當發生指令重排的時候可能會發生如下狀況:
1.分配內存空間;
2.將內存空間的地址賦值給對象的引用;
3.初始化對象;
這個時候我們就來考慮下多線程情況下可能發生的問題嘍,如果A線程正好初始化到了第(2)步的時候, 正好有其它線程B來獲取這個對象, 那線程B能不能看到這個由A初始化但是還沒初始化完畢的對象呢?答案是可能會看到這個未完全初始化的對象, 因為這里初始化的是一個共享變量,這個時候就會照成2種情況:
1.如果讀到的是null,反而沒問題了,接下來會等待鎖,然后再次判斷時不為null,最后返回單例。
2.如果讀到的不是null,那么壞了,按邏輯它就直接return instance了,這個instance還沒執行構造參數,去做事情的話,很可能就崩潰了。
注意:這個問題不容易出現理解下就好,不要和我一樣調試到懷疑人生。。。。。
四、總結
JMM就是一組規則,這組規則為了處理在並發編程可能出現的線程安全問題,並提供了內置的(happen-before原則)以及其外部可使用的同步手段(synchronized/volatile等),保證程序在執行多線程時候的原子性,可見性以及有序性。
volatile不能保證原子性;
歡迎加群:438836709
歡迎關注公眾號:
下一篇:spring IOC源碼解讀