原創聲明:做者:Arnold.zhao 博客園地址:https://www.cnblogs.com/zh94html
目錄:緩存
出現線程安全的問題本質是由於:安全
主內存和工做內存數據不一致性以及編譯器重排序致使。服務器
因此理解上述兩個問題的核心,對認知多線程的問題則具備很高的意義;多線程
CPU除了控制器、運算器等器件還有一個重要的部件就是寄存器。其中寄存器的做用就是進行數據的臨時存儲。併發
CPU的運算速度是很是快的,爲了性能CPU在內部開闢一小塊臨時存儲區域,並在進行運算時先將數據從內存複製到這一小塊臨時存儲區域中,運算時就在這一小快臨時存儲區域內進行。咱們稱這一小塊臨時存儲區域爲寄存器。jvm
CPU讀取指令是往內存裏面去讀取的,讀一條指令放到CPU中,CPU去執行,對內存的讀取速度比較慢,因此從內存讀取的速度去決定了這個CPU的執行速度的。因此不管咱們的CPU怎麼去升級,可是若是這方面速度沒有解決的話,其的性能也不會獲得多大的提高。函數
爲了彌補這個缺陷,因此添加了高速緩存的機制,如ARM A11的處理器,它的1級緩存中的容量是64KB,2級緩存中的容量是8M,
經過增長cpu高速緩存的機制,以此彌補服務器內存讀寫速度的效率問題;工具
JVM虛擬計算機平臺就相似於一個操做系統的角色,因此在具體實現上JVM虛擬機也的確是借鑑了不少操做系統的特色;性能
JAVA中線程的工做空間(working memory)就是CPU的寄存器和高速緩存的抽象描述,cpu在計算的時候,並不老是從內存讀取數據,它的數據讀取順序優先級 是:寄存器-高速緩存-內存;
而在JAVA的內存模型中也是同等的,Java內存模型中規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存(相似於CPU的高速緩存),線程的工做內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量,操做完成後再將變量寫回主內存。不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要在主內存來完成。基本關係以下圖:
注意:這裏的Java內存模型,主內存、工做內存與Java內存區域模型的Java堆、棧、方法區不是同一層次內存劃分,這二者基本上沒有關係。
在執行程序時,爲了提升性能,編譯器和處理器經常會對指令進行重排序。通常重排序能夠分爲以下三種:
舉例以下:
public class Singleton { public static Singleton singleton; /** * 構造函數私有,禁止外部實例化 */ private Singleton() {}; public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
如上,一個簡單的單例模式,按照對象的構造過程,實例化一個對象一、能夠分爲三個步驟(指令):
一、 分配內存空間。
二、 初始化對象。
三、 將內存空間的地址賦值給對應的引用。
可是因爲操做系統能夠對指令進行重排序,因此上面的過程也可能變爲以下的過程:
一、 分配內存空間。
二、 將內存空間的地址賦值給對應的引用。
三、 初始化對象 。
因此,若是出現併發訪問getInstance()方法時,則可能會出現,線程二判斷singleton是否爲空,此時因爲當前該singleton已經分配了內存地址,但其實並無初始化對象,則會致使return 一個未初始化的對象引用暴露出來,以此可能會出現一些不可預料的代碼異常;
固然,指令重排序的問題並不是每次都會進行,在某些特殊的場景下,編譯器和處理器是不會進行重排序的,但上述的舉例場景則是大機率會出現指令重排序問題(關於指令重排序的概念後續給出詳細的地址)
原創聲明:做者:Arnold.zhao 博客園地址:https://www.cnblogs.com/zh94
因此,如上可知,多線程在執行過程當中,數據的不可見性,原子性,以及重排序所引發的指令有序性 三個問題基本是多線程併發問題的三個重要特性,也就是咱們常說的:
併發的三大特性:原子性,有序性,可見性;
原子性:代碼操做是不是原子操做(如:i++ 看似一個代碼片斷,實際的執行中將會分爲三步執行,則必然是非原子化的操做,在多線程的場景中則會出現異常)
有序性:CPU執行代碼指令時的有序性;
可見性:因爲工做線程的內存與主內存的數據不一樣步,而致使的數據可見性問題;
可是,問題就真的有那麼複雜嗎?若是按照上面所說的問題,i++是非原子操做,就會出現併發異常的問題,new Object() 就會出現重排序的併發問題,那麼Java開發還能作嗎。。我隨便寫個方法代碼,豈不是就會出現併發問題?可是爲何我開發了這麼久的代碼,也沒有出現過方法併發致使的異常問題啊?
燒的麻袋;
這裏就要說明另一個問題,JVM的線程棧,JVM線程棧中是線程獨有的內存空間(如:程序計數器以線程棧幀)而線程棧幀中的局部變量表則用來存儲當前所執行方法的基本數據類型(包含 reference, returnAddress等),因此當方法在被線程執行的過程當中,相關的對象引用信息,以及基本類型的數據都是線程獨有的,並不會出現多個線程訪問時的併發問題,也就是簡單來講:一個方法內的變量定義以及方法內的業務代碼,是不會出現併發問題的。多個線程並不會共享一個方法內的變量數據,而是每一個方法內的定義都屬於當前該執行線程的獨有棧空間中。(因此經過Java線程棧的這一獨特特性天然當中則爲咱們省了不少事項;)
可是因爲咱們的線程的數據操做不可能每次都去訪問主存中的數據,對於線程所使用到的變量須要copy至線程內存中以增長咱們的執行速度,因此就引出了咱們上述所提到的併發問題的本質問題,線程工做空間和主內存的數據不一樣步而致使的數據共享時的可見性問題;
如:此時定義一個簡單的類
class Person{ int a = 1; int b = 2; public void change() { a = 3; b = a; } public void print() { String result = "b=" + b + ";a=" + a; System.out.println(result); } public static void main(String[] args) { while (true) { final Person test = new Person(); new Thread(() -> { Thread.sleep(10); test.change(); }).start(); new Thread(() -> { Thread.sleep(10); test.print(); }).start(); } } }
如上,假設此時多個線程同時訪問change()以及print() 方法,則可能會出現print所輸出的結果是:b=2;a=1或者b=3;a=3;這兩種都是正常現象,但還有多是會輸出結果是:b=2;a=3以及b=3;a=1;
Person類所定義的變量a和b,按照JVM內存區域劃分,在對象實例化後則都是存儲到數據堆中;
按照咱們上述關於線程工做內存的解釋來看,此時線程在執行change()方法和print()方法時,因爲兩個方法都有關於外部變量的引用,因此須要copy主內存中的這兩個變量副本到對應的線程工做內存中進行操做,執行完之後再同步至主內存中。
此時在A線程執行完change()方法後,a=3,b=3;但此時a=3在執行完成後尚未同步到主內存,但b=3此時已經提供至主內存了,那麼此時B線程執行print()數據輸出後,則獲得的是結果是:b=3;a=1;同理也能夠獲得b=2;a=3的可能性結果;因此此處則因爲線程共享變量的可見性問題,而致使了上述的問題;
正是因爲存在上述所提到的線程併發所可能引發的種種問題,因此JDK則也有了後續的一系列多線程玩法:ThreadLocal,CountDownLatch,ReentrantLock,Unsafe,synchronized,volatile,Executor,Future 這些供開發者在開發程序時用來對多線程保駕護航的助手類,以及JDK已經自身開發好的支持線程安全的一些工具類,StringBuffer,CopyOnWriteArrayList, ConcurrentHashMap,AtomicInteger等,供開發者開箱即用;後續針對這些JDK自身所提供的一些類的玩法會作進一步說明,順便系統整理下腦中的信息,造成有效的知識結構;End;
寫到這裏可能你依然會對線程工做內存和主內存的同步機制比較感興趣,則能夠參考這裏:
若是對上述所提到的線程棧的局部變量表等概念依然不是很清晰,則能夠參考這裏: