Java內存模型與線程


 

工做內存與主內存

 

Java內存模型(jmm)的出現是爲了各類操做系統和硬件的內存訪問的差別。

Java內存模型規定了變量不含局部變量,由於局部變量線程私有,不存在共享問題)都得存放在主內存中,而每一個線程對這些變量的操做都必須是從主內存中取出來並在工做內存中完成(如讀取、寫入的操做),不一樣線程之間不能訪問對方的工做內存。以下圖,展示了線程、主內存、工做內存之間的交互關係: java

加入一個工做內存的目的很明顯,就是爲了加快在內存中的操做數據的速度,由於工做內存優先存儲在寄存器和高速緩存中,這兩個操做的速度都遠遠快於主內存。 編程

主內存和工做內存之間交互的主要操做爲: 緩存

 

Lock(鎖定):做用於主內存的變量,將主內存該變量標記成當前線程私有的,其餘線程沒法訪問。 安全

Unlock(解鎖):做用於主內存的變量,解除主內存中該變量的鎖定狀態,讓他變成線程共享變量。 多線程

Read(讀取):做用於主內存的變量,將該變量讀取到當前線程的工做內存中,以便進行load操做。 併發

Load(加載):做用於工做內存中的變量,將read獲取到的變量載入工做內存的變量副本中。 app

Use(使用):做用於工做內存中的變量,虛擬機執行引擎在執行字節碼指令的時候,碰到了一個變量就會執行該操做,使用該變量。 jvm

Assgin(賦值):做用於工做內存中的變量,虛擬機執行引擎在執行字節碼指令的時候,碰到了變量賦值的指令就會執行該操做。 函數

Store(存儲):做用於工做內存中的變量,將工做內存中的變量放入主內存,以便進行write操做。 性能

Write(寫入):做用於主內存中的變量,將store獲得的變量放入主內存的變量中。

 

很明顯,若是兩個線程AB訪問同一個變量2的時候,都沒有鎖定。

A在工做內存改了變量值+1,而B此時並不能看到這個操做,B還覺得沒人改動這個值,就認爲本身是再原來的值上進行操做-1。算出來的值變成了多少呢?噢,並不知道,由於多是3,也多是1。這時,就出現了併發訪問的線程安全問題。

 

Volatile變量

        

         Volatilejvm提供的最輕量級的同步機制,可是要正確使用不容易。

         Volatile能夠保證兩個線程對變量操做的可見性,即一個線程操做的操做能夠被另外一線程看到。可是若是操做不是原子的,依然無法保證volatile同步的正確性。只有在下述狀況,纔可使用這個關鍵字:

對變量的寫入操做不依賴於該變量的當前值(好比a=0;a=a+1的操做,整個流程爲a初始化爲0,將a的值在0的基礎之上加1,而後賦值給a自己,很明顯依賴了當前值),或者確保只有單一線程修改變量。

該變量不會與其餘狀態變量歸入不變性條件中。(當變量自己是不可變時,volatile能保證安全訪問,好比雙重判斷的單例模式。但一旦其餘狀態變量參雜進來的時候,併發狀況就沒法預知,正確性也沒法保障)


    package org.project.loda.test;

/**
 * 
* @ClassName: Singleton 
* @Description: 基於雙重判斷的單例模式(普遍使用) 
* @author minjun
* @date 2015年6月5日 下午10:20:37 
*
 */
public class Singleton {

	private static volatile Singleton s;

	public static Singleton getInstance() {
		if (s == null) {
			synchronized (Singleton.class) {
				if (s == null) {
					s = new Singleton();
				}
			}
		}

		return s;
	}
}




Volatile還有個特性就是,他能夠禁止指令進行重排序優化。什麼是重排序?看看java併發編程實戰中的解釋:

各類操做延遲或者看似亂序執行的不一樣緣由,均可以歸爲重排序。
這是一個沒有間接性解釋,由於直接解釋很難以理解。下面看看這個程序:


若是按上到下順序判斷,線程one執行順序爲:a=1,x=b,線程other執行順序應該爲:b=1,y=a。而後完成計算,打印其值。可是,事實多是:

個人天哪?x=b居然在a=1以前執行了!!!發生了什麼?其實,這就是底層優化多線程程序的時候進行了重排序操做,致使亂序出現。最可怕的是,這還不是必然的,他可能發生,可能不發生,你根本預料不到!

因此,volatile自己強大的地方就是他還能預防這種狀況發生,雖然犧牲了一點性能,可是大大加強了程序的可靠性。可是記住,不要依賴於volatile,在合適的時候才使用他(上文已經說明),若是狀況不合適,就使用傳統的synchronized關鍵字同步共享變量的訪問,用來保證程序正確性(這個關鍵字的性能會隨着jvm不斷完善而不斷提高,未來性能會慢慢逼近volatile)。

 

Happens-Before原則

         說了這麼多,發現可使用volatilesynchronized關鍵字進行同步。可是,你不會平白無故就使用它們,存在競爭、線程安全問題的時候才應該考慮使用,可是如何判斷是否存在這些問題呢?下面介紹java內存模型中的一個重點原則——先行發生原則(Happens-Before),使用這個原則做爲依據,來指導你判斷是否存在線程安全和競爭問題

l  程序順序規則:在程序中,若是A操做在B操做以前(好比A代碼在B代碼上面,或者由A程序調用B程序),那麼在這個線程中,A操做將在B操做以前執行。

l  管理鎖定規則:一個unlock操做先於後面對同一個鎖的lock操做以前執行。

Volatile變量規則:對一個volatile變量的寫操做必須在對該變量的讀操做以前發生。

l  線程啓動規則:線程的Thread.start必須在該線程全部其餘操做以前發生

l  線程終止規則:線程中全部操做都先行發生於該線程的終止檢測。能夠經過Thread.join()方法結束、Thread.isAlive()的返回值判斷線程是否終止。

l  線程中斷規則:對線程interrupt()方法的調用必須在被中斷線程的代碼檢測到interrupt調用以前執行。

l  對象終結規則:對象的初始化(構造函數的調用)必須在該對象的finalize()方法完成。

l  傳遞性:若是A先行發生於BB先行發生於C,那麼A先行發生於C

這些操做是無需使用任何同步手段就能保證成立的先行發生規則。若是要線程AB,須要B能看到A操做的結果(不管二者是否在一個線程當中),須要AB知足Happens-Before關係,若是兩個操做不存在Happens-Before關係,JVM會對他們進行任意重排序。當AB在同一個線程中,或者兩個線程使用一樣的鎖,他們就能知足Happens-Before,若是使用不一樣鎖,就不知足。

線程

         線程也叫做輕量級進程,是大多現代操做系統的基本調度單位。在同一個進程中,多個線程共享內存空間,所以須要足夠的同步機制才能保證正常訪問。每一個線程自己都有各自的程序計數器、棧和局部變量等。在java中使用線程調度的方式是搶佔式的,須要由操做系統分配執行時間,線程自己沒法決定(例如java中,只有Thread.yield()可讓出本身的執行時間,可是並無提供能夠主動獲取執行時間的操做)。雖然java中線程調度由系統執行,可是仍是能夠經過設置線程優先級來「建議」操做系統多給某些線程分配執行時間(而後,這並不必定就能保證高優先級的先執行,因此不太靠譜...)。

         Java定義了以下幾種線程狀態,一個線程有且僅有一個:


上圖指定了幾種狀態以及達到這些狀態所須要經歷的過程,這裏就不給出解釋了,有基礎的同窗應該很容易看懂這些狀態。

 參考文獻:java併發編程實戰

                深刻java虛擬機(JVM高級特性與最佳實踐

相關文章
相關標籤/搜索