關於java的volatile關鍵字與線程棧的內容以及單例的DCL

  用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

java線程內存模型

線程、工做內存、主內存三者之間的交互關係圖:

 

key edeas

全部線程共享主內存
每一個線程有本身的工做內存
refreshing local memory to/from main memory must  comply to JMM rules

 

產生線程安全的緣由

線程的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 實例是可訪問的;在線程消失以後,其線程局部實例的全部副本都會被垃圾回收(除非存在對這些副本的其餘引用)。

 

3、java內存模型

http://blog.csdn.net/jinyongqing/article/details/21343629

java中,線程之間的通訊是經過共享內存的方式,存儲在堆中的實例域,靜態域以及數組元素均可以在線程間通訊。java內存模型控制一個線程對共享變量的改變什麼時候對另外一個線程可見。
線程間的共享變量存在主內存中,而對於每個線程,都有一個私有的工做內存。工做內存是個虛擬的概念,涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化,總之就是指線程的本地內存。存在線程本地內存中的變量值對其餘線程是不可見的。
若是線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟,如圖所示:
1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
2. 而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。 

 http://ifeve.com/wp-content/uploads/2013/01/221.png

 
關於volatile變量
因爲java的內存模型中有工做內存和主內存之分,因此可能會有兩種問題:
(1)線程可能在工做內存中更改變量的值,而沒有及時寫回到主內存,其餘線程從主內存讀取的數據仍然是老數據
(2)線程在工做內存中更改了變量的值,寫回主內存了,可是其餘線程以前也讀取了這個變量的值,這樣其餘線程的工做內存中,此變量的值沒有被及時更新。
爲了解決這個問題,可使用同步機制,也能夠把變量聲明爲volatile,volatile修飾的成員變量有如下特色:
(1)每次對變量的修改,都會引發處理器緩存(工做內存)寫回到主內存。
(2)一個工做內存回寫到主內存會致使其餘線程的處理器緩存(工做內存)無效。
基於以上兩點,若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。
此外,java虛擬機規範(jvm spec)中,規定了聲明爲volatile的long和double變量的get和set操做是原子的。這也說明了爲何將long和double類型的變量用volatile修飾,就能夠保證對他們的賦值操做的原子性了
ps:最上面那個加的例子貌似說明了第二種狀況是不能使用volatile解決的,並且換成long或者double都是同樣的結果,沒效果。

 

關於volatile變量的使用建議:多線程環境下須要共享的變量採用volatile聲明;若是使用了同步塊或者是常量,則沒有必要使用volatile。
 
java內存模型與synchronized關鍵字
synchronized關鍵字強制實施一個互斥鎖,使得被保護的代碼塊在同一時間只能有一個線程進入並執行。固然synchronized還有另一個 方面的做用:在線程進入synchronized塊以前,會把工做存內存中的全部內容映射到主內存上,而後把工做內存清空再從主存儲器上拷貝最新的值。而 在線程退出synchronized塊時,一樣會把工做內存中的值映射到主內存,但此時並不會清空工做內存。這樣一來就能夠強制其按照上面的順序運行,以 保證線程在執行完代碼塊後,工做內存中的值和主內存中的值是一致的,保證了數據的一致性!  
因此由synchronized修飾的set與get方法都是至關於直接對主內存進行操做,不會出現數據一致性方面的問題。
 
 
關於CAS
CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。 若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。不然,處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該 位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前 值。)CAS 有效地說明了「我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。」
 
爲何CAS能夠用於同步?
例如,有一個變量i=0,Thread-1和Thread-2都對這個變量執行自增操做。 可能會出現Thread-1與Thread-2同時讀取i=0到各自的工做內存中,而後各自執行+1,最後將結果賦予i。這樣,雖然兩個線程都對i執行了自增操做,可是最後i的值爲1,而不是2。
解決這個問題使用互斥鎖天然能夠。可是也可使用CAS來實現,思路以下:
自增操做能夠分爲兩步:(1)從內存中讀取這個變量的當前值(2)執行(變量=上一步取到的當前值+1)的賦值操做。
 
多線程狀況下,自增操做出現問題的緣由就是執行(2)的時候,變量在主內存中的值已經不等於上一步取到的當前值了,因此賦值時,用CompareAndSet操做代替Set操做:首先比較一下內存中這個變量的值是否等於上一步取到的當前值,若是等於,則說明能夠執行+1運算,並賦值;若是不等於,則說明有其餘線程在此期間更改了主內存中此變量的值,上一步取出的當前值已經失效,此時,再也不執行+1運算及後續的賦值操做,而是返回主內存中此變量的最新值。「比較並交換(CAS)」操做是原子操做,它使用平臺提供的用於併發操做的硬件原語。
 
 
總結:volatile關鍵字只保證每次對該變量的修改,都反映到主內存中,且
相關文章
相關標籤/搜索