參考文章:JMM內存模型算法
多線程:指的是這個程序(一個進程)運行時產生了不止一個線程數據庫
並行與併發:緩存
並行:多個cpu實例或者多臺機器同時執行一段處理邏輯,是真正的同時。安全
併發:經過cpu調度算法,讓用戶看上去同時執行,實際上從cpu操做層面不是真正的同時。併發每每在場景中有公用的資源,那麼針對這個公用的資源每每產生瓶頸,咱們會用TPS或者QPS來反應這個系統的處理能力。性能優化
併發與並行數據結構
線程安全:常常用來描繪一段代碼。指在併發的狀況之下,該代碼通過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,咱們只須要關注系統的內存,cpu是否是夠用便可。反過來,線程不安全就意味着線程的調度順序會影響最終結果。多線程
同步:Java中的同步指的是經過人爲的控制和調度,保證共享資源的多線程訪問成爲線程安全,來保證結果的準確。如上面的代碼簡單加入@synchronized關鍵字。在保證結果準確的同時,提升性能,纔是優秀的程序。線程安全的優先級高於性能。併發
Java線程之間的通訊由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在,它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。jvm
線程間通訊的步驟:函數
首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。
以下圖:
解釋下上文所說的主內存和本地內存:
首先,JVM將內存組織爲主內存和工做內存兩個部分。
主內存主要包括本地方法區和堆。每一個線程都有一個工做內存,工做內存中主要包括兩個部分,一個是屬於該線程私有的棧和對主存部分變量拷貝的寄存器(包括程序計數器PC和cup工做的高速緩存區)。
1.全部的變量都存儲在主內存中(虛擬機內存的一部分),對於全部線程都是共享的。
2.每條線程都有本身的工做內存,工做內存中保存的是主存中某些變量的拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的變量。
3.線程之間沒法直接訪問對方的工做內存中的變量,線程間變量的傳遞均須要經過主內存來完成。
見下圖:
正常的JVM模型以下:
省略了本地方法棧(本地方法棧(Native MethodStacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。),上圖所提到的棧都屬於虛擬機棧。
說到多線程就不能不提到線程安全這個概念:
當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。
因爲線程訪問無狀態對象的行爲並不會影響其它線程中操做的正確性,所以無狀態對象是線程安全的。大多數Servlet都是無狀態的,從而極大地下降了在實現Servlet線程安全性時的複雜性。只有當Servlet在處理請求時須要保存一些信息,線程安全才會成爲一個問題。
要編寫線程安全的代碼,其核心在於要對狀態訪問操做進行管理,特別是對共享的和可變的狀態的訪問。對象的狀態是指存儲在狀態變量(例如實例或靜態域)中的數據,對象的狀態可能包括其餘依賴對象的域。「共享」意味着變量能夠由多個線程同時訪問,而「可變」則意味着變量的值在其生命週期內能夠發生變化。訪問某個變量的代碼越少,就越容易確保對變量的全部訪問都實現正確同步,同時也更容易找出變量在哪些條件下被訪問,Java語言並無強制要求將狀態都封裝在類中,開發人員徹底能夠將狀態保存在某個公開的域(甚至公開的靜態域)中,或者提供一個對內部對象的公開引用。然而,程序狀態的封裝性越好,就越容易實現程序的線程安全性,而且代碼的維護人員也越容易保持這種方式。當設計線程安全類時,良好的面向對象技術、不可修改性,以及明晰的不變性規範都能起到必定的幫助做用。
同時保證了線程的原子性和可見性便可保證線程的安全性。
原子性
就像數據庫裏的定義同樣,原子性就是一個操做(多是須要多步完成的複合操做)不能被打斷,一旦開始執行直到執行完其餘線程或多核都必須等待。好比」i++」表達式,就不是原子的,彙編後會發現由三條指令(讀取,修改,寫入)完成,每一條指令完成後均可能被中斷。說到原子性,通常會提到可見性,這二者其實沒有任何聯繫,但這兩個因素確是同時影響到多線程安全的特性。只具有原子性或可見性並不能保證線程安全(注意synchronized同時保證了原子性和可見性,只保證原子性可能結果並無同步到主存,其餘線程不可見)。可見性跟jvm的內存結構有關係,前面給出了jvm內存結構圖,各個線程或多核對同一個變量有備份(在線程的工做內存中或核的寄存器中,爲了節省IO通訊等),致使跟jvm主存中的變量值不一致。這樣作的目的是爲了提升性能。固然在多線程中就可能形成問題,就要用同步來解決了。
可見性
保證內存可見性就是但願確保當一個線程修改了對象狀態後,其餘線程可以看到發生的狀態變化。在一個單線程程序中,若是首先改變一個變量的值,再讀取該變量的值的時候,所讀取到的值就是上次寫操做寫入的值。也就是說前面操做的結果對後面的操做是確定可見的。可是在多線程程序中,若是不使用必定的同步機制,就不能保證一個線程所寫入的值對另一個線程是可見的。形成這種狀況的緣由可能有下面幾個:
CPU 內部的緩存:如今的CPU通常都擁有層次結構的幾級緩存。CPU直接操做的是緩存中的數據,並在須要的時候把緩存中的數據與主存進行同步。所以在某些時刻,緩存中的數據與主存內的數據多是不一致的。某個線程所執行的寫入操做的新值可能當前還保存在CPU的緩存中,尚未被寫回到主存中。這個時候,另一個線程的讀取操做讀取的就仍是主存中的舊值。
CPU的指令執行順序:在某些時候,CPU可能改變指令的執行順序。這有可能致使一個線程過早的看到另一個線程的寫入操做完成以後的新值。
編譯器代碼重排:出於性能優化的目的,編譯器可能在編譯的時候對生成的目標代碼進行從新排列。
事實上,在沒有同步的狀況下,編譯器、處理器以及運行時均可能對操做的執行順序進行一些意想不到的調整。在缺少足夠同步的多線程程序中,要想對內存操做的執行順序進行判斷,幾乎沒法獲得正確的結論。
Java內存模型經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性。volatile保證新值能當即同步到主內存,每次使用前當即從主內存刷新。volatile,sychronized和final均可以實現可見性。synchronized可見性是經過對一個變量執行unlock操做以前,必須把變量同步回主內存實現的。final關鍵字是很是重要而事實上卻常常被忽視其做爲同步的做用。本質上講,final可以作出以下保證:當你建立一個對象時,使用final關鍵字可以使得另外一個線程不會訪問處處於「部分建立」的對象,不然是會可能發生的。這是 由於,當用做對象的一個屬性時,final有着以下的語義:當構造函數結束時,final類型的值是被保證其餘線程訪問該對象時,它們的值是可見的。使用final是所謂的安全發佈的一種方式,在一個線程中建立它,同時另外一個線程在以後的某時刻能夠引用到該新建立的對象。當JVM調用對象的構造函數時,它必須將各成員賦值,同時存儲一個指向該對象的指針。就像其餘任何的數據寫入同樣,這多是亂序的,就被寫入到主存並被訪問到了。這樣會致使另外一個線程看到了一個不合法或不完整的對象。而final能夠防止此類事情的發生:若是某個成員是final的,JVM規範作出以下明確的保證:一旦對象引用對其餘線程可見,則其final成員也必須正確的賦值了。final的對象引用,對象的final成員的值在當退出構造函數時,他們也是最新的。這意味着:final類型的成員變量的值,包括那些用final引用指向的collections的對象,是讀線程安全而無需使用synchronization