系列閱讀
1.深入理解Java虛擬機-GC&運行時數據區
2.深入理解Java虛擬機-類文件結構及加載
3.深入理解Java虛擬機-內存模型及多線程
1. Java內存模型
主內存(Main Memory)是各個線程共享的內存區域,所有的變量都存儲在主內存中。線程間變量值的傳遞需要通過主內存來完成。
工作內存(Working Memory)是每條線程都有屬于自己的區域,工作內存保存了被該線程所使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、賦值等)等都必須在工作內存中進行,而不能直接讀寫主內存中的變量。
勉強來說,主內存對應于物理硬件的內存,工作內存優先存儲于寄存器和高速緩存中,因為程序運行時主要訪問讀寫的是工作內存。
主內存與工作內存之間的交互協議,即讀寫同步的操作是原子的,不可再分的,包括以下8中操作:lock/unlock/read/write作用于主內存變量,use/assign/store/load作用于工作內存。
2. 線程同步
valatile同步
可以說是JVM中最輕量級的同步機制。
保證變量對所有線程的可見性,而普通變量不能保證這一點。
禁止指令重排序優化,保證變量賦值操作的順序與程序代碼的執行順序一致。
優點:volatile變量讀操作與普通變量幾無差別,寫操作時由于在本地代碼中插入需要內存屏障質量來保證處理器不發生亂序執行,所以會慢一點。
volatile與鎖之間選擇的唯一依據是volatile能否滿足使用場景的需求。
Java內存模型3大特性
- 原子性
可大致認為基本數據類型的訪問讀寫是具備原子性的。synchronized塊之間具備原子性。 - 可見性
指當一個線程改變了此值,新值對其他線程立即可見。Java內存模型通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作為傳遞媒介的方式來實現可見性的。volatile/普通變量/synchronized/final。 - 有序性
如果在本線程內觀察,所有的操作都是有序的。如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現為串行的語義”,后半句是指“指令重排序”現象和“工作內存與主內存同步延遲”的現象。valatile及synchronized可保證線程之間操作的有序性。synchronized規定了“一個變量在同一時刻只允許一條線程對其進行lock操作”。
線程的實現
線程的引入可以把一個進程的資源分配和執行調度分開,線程既可共享進程資源(內存地址、文件I/O等),也可獨立調度(線程是CPU調度的基本單位)。
實現線程主要有3種方式:
-
使用內核線程實現
輕量級進程(Light Weight Process, LWP)就是通常意義上的線程,每個LWP都由一個內核線程(Kernel-Level Thread,KTL)支持。
輕量級進程與內核線程之間1:1的關系 -
使用用戶線程實現
廣義上來說,一個線程只要不是內核線程,就可以認為是用戶線程(User Thread,UT)。用戶進程的建立、同步、銷毀和調度完全在用戶態中進行,不需要內核的幫助,所以,所有線程都需用戶程序自己處理的話會異常困難。
進程與用戶線程之間1:N的關系 -
使用用戶線程加輕量級進程混合實現
這種混合實現下既存在用戶線程也存在輕量級進程。用戶線程完全建立在用戶空間中,因此用戶線程的創建、切換、析構等操作依然廉價,并且可以支持大規模的用戶線程并發。而操作系統提供支持的輕量級進程則作為用戶線程和內核線程之間橋梁,這樣可以使用內核提供的線程調度功能及處理器映射,并且用戶線程的系統調用要通過輕量級進程來完成,大大降低了整個進程被完全阻塞的風險。
用戶線程與輕量級進程之間N:M的關系
線程調度
多線程系統的線程調度是指系統為線程分配處理器使用權的過程,主要調度方式為以下兩種:
協同式調度:線程的執行時間由線程本身來控制;
搶占式調度:每個線程將有系統來分配執行時間。
線程的狀態轉換可參見Java并發編程學習筆記
3. 線程安全
當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的操作,單次調用都可以獲得正確的結果,那這個對象就是線程安全的。
線程安全的實現方法
- 互斥同步
同步是指在多個線程并發訪問共享數據時,保證共享數據在同一個時刻只被一個(或者是一些,使用信號量的時候)線程使用。而互斥是實現同步的一種手段,臨界區、互斥量、信號量都是主要的互斥實現方式。Java中可使用synchronized關鍵字和RetrantLock(重入鎖)來實現同步,具體參見JAVA鎖機制。 - 非阻塞同步
互斥同步主要問題是進行線程阻塞和喚醒所帶來的性能問題,因此這種同步也叫阻塞同步。
非阻塞同步是基于沖突檢測的樂觀并發策略,先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果有爭用,產生了沖突,那就再采取其他的補償措施。這種實現大都不需要把線程掛起。為了讓操作和沖突檢測這兩個步驟具備原子性,需要硬件指令集的發展和支持。 - 無同步方案
同步只是保證共享數據爭用時的正確性的手段。如果一個方法不涉及共享數據則無需任何同步措施去保證正確性。比如可重入代碼和縣城本地存儲。
操作共享的數據類型
不可變
不可變(Immutable)對象一定是線程安全的。如果共享數據是基本數據類型,只要定義用final修飾則是不可變;如果是一個對象,需要保證對象的行為不會對其狀態產生任何影響,比如String/Number部分子類/Long/Double/BigInteger/DigDecimal等。絕對線程安全
一個類不管運行時環境如何,調用者都不需要任何額外的同步措施。相對線程安全
需要保證對這個對象單獨的操作是線程安全的,在調用的時候不需要做額外的保障措施。Java中大部分線程安全類都屬于這種,例如Vector/HashTable/Collections的synchronizedCollection()方法包裝的集合等。線程兼容
對象本身并不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在并發環境中可以安全地使用。比如Vector/ArrayList/HashMap等。線程對立
無論調用端是否采取了同步措施,都無法在多線程環境中并發使用的代碼。Java中很少出現。
注:主要內容摘錄自書籍 深入理解Java虛擬機,周志明 著