用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最新的值。volatile很容易被誤用,用來進行原子性操做。java
package com.guangshan.test; public class TestVolatile { public static int count = 0; public static void inc () { try { Thread.sleep(1); } catch (Exception e) { } count++; } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { public void run() { TestVolatile.inc(); } }).start(); } System.out.println(count); Thread.sleep(1000); System.out.println(count); } }
這段代碼,最後的count值頗有可能不爲1000(main函數所在的線程爲主線程,主線程的最後一句代碼執行後,會進入Thread.exit()方法,該方法會強制終止全部該線程建立的線程),在sleep(1000)後,其餘加的線程已經結束了,按理講,這裏的count應該爲1000的,可是爲何不是1000呢?程序員
不少人覺得,這個是多線程併發問題,只須要在變量count以前加上
volatile
就能夠避免這個問題,那咱們在修改代碼看看,看看結果是否是符合咱們的指望。
數組
加入volatile以後,仍然有可能不是1000,下面咱們分析一下緣由緩存
在 java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區域是jvm虛擬機棧,每個線程運行時都有一個線程棧,線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先經過對象的引用找到對應在堆內存的變量的值,而後把堆內存變量的具體值load到線程本地內存中,創建一個變量副本,以後線程就再也不和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,在修改完以後的某一個時刻(線程退出以前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。下面一幅圖描述這寫交互安全
read and load 從主存複製變量到當前工做內存
use and assign 執行代碼,改變共享變量值
store and write 用工做內存數據刷新主存相關內容多線程
其中use and assign 能夠屢次出現併發
可是這一些操做並非原子性,也就是 在read load以後,若是主內存count變量發生修改以後,線程工做內存中的值因爲已經加載,不會產生對應的變化,因此計算出來的結果會和預期不同jvm
對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工做內存的值是最新的函數
例如假如線程1,線程2 在進行read,load 操做中,發現主內存中count的值都是5,那麼都會加載這個最新的值優化
在線程1堆count進行修改以後,會write到主內存中,主內存中的count變量就會變爲6
線程2因爲已經進行read,load操做,在進行運算以後,也會更新主內存count的變量值爲6
致使兩個線程即便用volatile關鍵字修改以後,仍是會存在併發的狀況。
synchronize關鍵字修飾的代碼塊,會自動與主內存同步資源,即在退出sync代碼塊時,主內存資源自動同步爲最新的資源(猜想)
單例的DCL(雙重檢查加鎖)
public class SingletonKerriganD { /** * 單例對象實例 */ private static SingletonKerriganD instance = null; public static SingletonKerriganD getInstance() { if (instance == null) { synchronized (SingletonKerriganD.class) { if (instance == null) { instance = new SingletonKerriganD(); } } } return instance; } }
看起來這樣已經達到了咱們的要求,除了第一次建立對象以外,其餘的訪問在第一個if中就返回了,所以不會走到同步塊中。已經完美了嗎?
咱們來看看這個場景:假設線程一執行到instance = new SingletonKerriganD()這句,這裏看起來是一句話,但實際上它並非一個原子操做(原子操做的意思就是這條語句要麼就被執行完,要麼就沒有被執行過,不能出現執行了一半這種情形)。事實上高級語言裏面非原子操做有不少,咱們只要看看這句話被編譯後在JVM執行的對應彙編代碼就發現,這句話被編譯成8條彙編指令,大體作了3件事情:
1.給Kerrigan的實例分配內存。
2.初始化Kerrigan的構造器
3.將instance對象指向分配的內存空間(注意到這步instance就非null了)。
可是,因爲Java編譯器容許處理器亂序執行(out-of-order),以及JDK1.5以前JMM(Java Memory Medel)中Cache、寄存器到主內存回寫順序的規定,上面的第二點和第三點的順序是沒法保證的,也就是說,執行順序多是1-2-3也多是1-3-2,若是是後者,而且在3執行完畢、2未執行以前,被切換到線程二上,這時候instance由於已經在線程一內執行過了第三點,instance已是非空了,因此線程二直接拿走instance,而後使用,而後瓜熟蒂落地報錯,並且這種難以跟蹤難以重現的錯誤估計調試上一星期都未必能找得出來,真是一茶几的杯具啊。
DCL的寫法來實現單例是不少技術書、教科書(包括基於JDK1.4之前版本的書籍)上推薦的寫法,其實是不徹底正確的。的確在一些語言(譬如C語言)上DCL是可行的,取決因而否能保證二、3步的順序。在JDK1.5以後,官方已經注意到這種問題,所以調整了JMM、具體化了volatile關鍵字,所以若是JDK是1.5或以後的版本,只須要將instance的定義改爲「private volatile static SingletonKerriganD instance = null;」就能夠保證每次取instance都從主內存讀取,就可使用DCL的寫法來完成單例模式。
2、如下來自http://rainyear.iteye.com/blog/1734311
線程、工做內存、主內存三者之間的交互關係圖:
key edeas
線程的working memory是cpu的寄存器和高速緩存的抽象描述:如今的計算機,cpu在計算的時候,並不老是從內存讀取數據,它的數據讀取順序優先級 是:寄存器-高速緩存-內存。線程耗費的是CPU,線程計算的時候,原始的數據來自內存,在計算過程當中,有些數據可能被頻繁讀取,這些數據被存儲在寄存器和高速緩存中,當線程計算完後,這些緩存的數據在適當的時候應該寫回內存。當多個線程同時讀寫某個內存數據時,就會產生多線程併發問題,涉及到三個特 性:原子性,有序性,可見性。 支持多線程的平臺都會面臨 這種問題,運行在多線程平臺上支持多線程的語言應該提供解決該問題的方案。
JVM是一個虛擬的計算機,它也會面臨多線程併發問題,java程序運行在java虛擬機平臺上,java程序員不可能直接去控制底層線程對寄存器高速緩存內存之間的同步,那麼java從語法層面,應該給開發人員提供一種解決方案,這個方案就是諸如 synchronized, volatile,鎖機制(如同步塊,就緒隊 列,阻塞隊列)等等。這些方案只是語法層面的,但咱們要從本質上去理解它;
每一個線程都有本身的執行空間(即工做內存),線程執行的時候用到某變量,首先要將變量從主內存拷貝的本身的工做內存空間,而後對變量進行操做:讀取,修改,賦值等,這些均在工做內存完成,操做完成後再將變量寫回主內存;
各個線程都從主內存中獲取數據,線程之間數據是不可見的;打個比方:主內存變量A原始值爲1,線程1從主內存取出變量A,修改A的值爲2,在線程1未將變量A寫回主內存的時候,線程2拿到變量A的值仍然爲1;
這便引出「可見性」的概念:當一個共享變量在多個線程的工做內存中都有副本時,若是一個線程修改了這個共享變量的副本值,那麼其餘線程應該可以看到這個被修改後的值,這就是多線程的可見性問題。
普通變量狀況:如線程A修改了一個普通變量的值,而後向主內存進行寫回,另一條線程B在線程A回寫完成了以後再從主內存進行讀取操做,新變量的值纔會對線程B可見;
如何保證線程安全
編寫線程安全的代碼,本質上就是管理對狀態(state)的訪問,並且一般都是共享的、可變的狀態。這裏的狀態就是對象的變量(靜態變量和實例變量)
線程安全的前提是該變量是否被多個線程訪問, 保證對象的線程安全性須要使用同步來協調對其可變狀態的訪問;如果作不到這一點,就會致使髒數據和其餘不可預期的後果。不管什麼時候,只要有多於一個的線程訪問給定的狀態變量,並且其中某個線程會寫入該變量,此時必須使用同步來協調線程對該變量的訪問。Java中首要的同步機制是synchronized關鍵字,它提供了獨佔鎖。除此以外,術語「同步」還包括volatile變量,顯示鎖和原子變量的使用。
在沒有正確同步的狀況下,若是多個線程訪問了同一個變量,你的程序就存在隱患。有3種方法修復它:
l 不要跨線程共享變量;
l 使狀態變量爲不可變的;或者
l 在任何訪問狀態變量的時候使用同步。
volatile要求程序對變量的每次修改,都寫回主內存,這樣便對其它線程課件,解決了可見性的問題,可是不能保證數據的一致性;特別注意:原子操做:根據Java規範,對於基本類型的賦值或者返回值操做,是原子操做。但這裏的基本數據類型不包括long和double, 由於JVM看到的基本存儲單位是32位,而long 和double都要用64位來表示。因此沒法在一個時鐘週期內完成
通俗的講一個對象的狀態就是它的數據,存儲在狀態變量中,好比實例域或者靜態域;不管什麼時候,只要多於一個的線程訪問給定的狀態變量。並且其中某個線程會寫入該變量,此時必須使用同步來協調線程對該變量的訪問;
同步鎖:每一個JAVA對象都有且只有一個同步鎖,在任什麼時候刻,最多隻容許一個線程擁有這把鎖。
當一個線程試圖訪問帶有synchronized(this)標記的代碼塊時,必須得到 this關鍵字引用的對象的鎖,在如下的兩種狀況下,本線程有着不一樣的命運。
一、 假如這個鎖已經被其它的線程佔用,JVM就會把這個線程放到本對象的鎖池中。本線程進入阻塞狀態。鎖池中可能有不少的線程,等到其餘的線程釋放了鎖,JVM就會從鎖池中隨機取出一個線程,使這個線程擁有鎖,而且轉到就緒狀態。
二、 假如這個鎖沒有被其餘線程佔用,本線程會得到這把鎖,開始執行同步代碼塊。
(通常狀況下在執行同步代碼塊時不會釋放同步鎖,但也有特殊狀況會釋放對象鎖
如在執行同步代碼塊時,遇到異常而致使線程終止,鎖會被釋放;在執行代碼塊時,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池中)
Synchronized關鍵字保證了數據讀寫一致和可見性等問題,可是他是一種阻塞的線程控制方法,在關鍵字使用期間,全部其餘線程不能使用此變量,這就引出了一種叫作非阻塞同步的控制線程安全的需求;
ThreadLocal 解析
顧名思義它是local variable(線程局部變量)。它的功用很是簡單,就是爲每個使用該變量的線程都提供一個變量值的副本,是每個線程均可以獨立地改變本身的副本,而不會和其它線程的副本衝突。從線程的角度看,就好像每個線程都徹底擁有該變量。
每一個線程都保持對其線程局部變量副本的隱式引用,只要線程是活動的而且 ThreadLocal 實例是可訪問的;在線程消失以後,其線程局部實例的全部副本都會被垃圾回收(除非存在對這些副本的其餘引用)。
http://blog.csdn.net/jinyongqing/article/details/21343629