JVM之線程安全與鎖優化(十三)

      在軟件業發展初期,程序編寫都是以算法爲核心的,程序員會把數據和過程分別作爲獨立的部分來考慮,數據代表問題空間中的客體,程序代碼則用於處理這些數據,這種思維的方式直接站在計算機的角度去抽象問題和結局問題,稱爲面向過程的編程思想。
      面向對象的編程思想是站在現實世界的角度去抽象和解決問題,它把數據和行爲都看做是對象的一部分,這樣可以讓程序員能以符合現實世界的思維方式來編寫和組織程序。
線程安全
定義:當多個線程訪問一個對象時,如果不用考慮這些線程在運行環境下的調度和交替執行,也不需要進行額外的同步,或者在調用其他方法協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的。(代碼本身封裝了所有的必要的正確性保障手段(如互斥同步等),令調用者無需關心多線程問題,更無需自己採取任何措施來。
Java中的線程安全
我們這裏討論的線程安全就限定於多個線程之間存在共享數據訪問這個前提。爲了更深入的理解線程安全,我們這裏不把線程當做非真即假的二元拍它項,按照安全程度強弱排序,java各種操作共享數據分爲五類:
不可變
絕對線程安全
相對線程安全
線程兼容
線程獨立
》不可變
不可變對象無論是對象的方法實現還是方法的調用者,都不需要採取任何線程安全保障措施,所以它一定是線程安全的。比如final關鍵字可見性,java中的String對象,它就是一個不可變對象,無論是調用它的substring(),replace(),concat等都不會改變原來的值,只會返回一個新構造的字符號對象。保證對象行爲不會影響自己的狀態,最簡單的方式就是帶有狀態的變量標記爲final,這樣構造函數結束之後,它是不可變的。如Integer 中int值定義爲:final int value;
》絕對的線程安全
絕對的線程安全就是最上面給出的定義,但是那個太嚴格,達到絕對的線程安全要花費很大代價。Java API中標註自己是線程安全的類,大多數都不是絕對的線程安全。比如下面這個例子:
java.util.Vector是一個線程安全的容器,因爲它的add、get、size方法都是被synchronized所修飾,儘管效率低,但是線程安全。(錯誤的理解:這樣只能保證同一個方法,同一個時間只能被同一個線程調用,但是並不能保證其他線程不能調用別的方法,正確的理解: synchronized修飾的方法在vector中是實例方法,因此加鎖對象就是代碼中創建實例vector,即當一個線程執行remove時,別的線程不能執行get。)

import java.util.Vector;

public class ThreadVector {
    private static Vector<Integer> vector=new Vector<Integer>();
    public static void main(String[] args) {
        while(true){
            for(int i=0;i<4;i++){
                vector.add(i);
            }
            Thread removedThread =new Thread( new Runnable(){

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    //同一個對象加鎖,只有一個線程釋放鎖後,後一個線程才能執行,相當於串行執行兩段代碼
                    //synchronized(vector){
                    for(int i=0;i<vector.size();i++){
                        vector.remove(i);
                    }
                //}
                }});
           Thread printThread=new Thread(new Runnable(){

            @Override
            public void run() {
                // TODO Auto-generated method stub
                //加鎖同步
                //synchronized(vector){
                for(int i=0;i<vector.size();i++){
                    System.out.println(vector.get(i));
                }
            //}
            }});
           removedThread.start();
           printThread.start();
           while(Thread.activeCount()>20);
        }

    }

}

部分運算結果(隔一段時間就會出現類似如下錯誤異常):
……..
1Exception in thread “Thread-1128” java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1

at java.util.Vector.remove(Vector.java:827)
at com.jvm.threadsecurity.ThreadVector$1.run(ThreadVector.java:18)
at java.lang.Thread.run(Thread.java:744)

造成這個結果的原因是當一個線程在執行remove方法後,另一個線程在執行獲取get方法,而get方法在

    public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
    }

從上面可以看出,出錯是因爲 在獲取前先判斷elementCount,使得條件成立index >= elementCount,從而拋出異常。
既然對實例對象進行加鎖了,同一時刻要麼remove方法執行,要麼get方法執行,不會同時執行,爲什麼還會出現在這種情況?
根據代碼可以看出其實這和vector無關,主要是因爲for循環出的問題:

假如某個時刻,vector中最大下標元素爲8,而printThread線程在執行代碼for(int i=0;i<vector.size();i++)中i=8,
且小於vector.size9)時,cpu執行時間結束,這個時候還沒有獲得執行remove方法,也因此沒有獲得鎖;接下來當
removedThread線程在執行remove方法時,比如在執行i=8時,先獲得vector鎖,假如在剛執行remove(8)後,
cpu時間片結束,,則它會把vector鎖釋放,這個時候printThread線程就獲得鎖接着執行get方法,
即get(8),這個時候就會出現這種下標越界情況。

要保證代碼安全執行,加鎖同步,如上面註釋代碼加上,這樣是對整個for循環進行同步,也就不會出現i越界情況。
注意:sleep時線程不會釋放鎖資源(如果持有),當cpu時間執行結束,根據情況分析,如果是同步代碼塊也不會釋放鎖。
》相對的線程安全
相對的線程安全就是我們通常意義上講的線程安全,他保證對這個對象單獨操作是線程安全的,我們在調用的時候不需要做額外的保障措施,但對於一些特定順序的連續調用,就可能需要在調用端額外的使用同步手段來保證調用正確性。上面代碼就是相對安全的案例。
》線程兼容
線程兼容是指對象本身不是線程安全的,但是調用端正確的使用同步手段來保證對象在併發情況下可以安全的使用。如ArrayList和HashMap等。
》線程獨立
線程獨立是指調用端無論是否採取同步措施,都無法在多線程下併發使用代碼。這種情況很少,也儘量避免。比如Thread類中的suspend和resume方法,如果兩個對象同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢復線程,併發情況下,是不可能同步的。都會存在死鎖風險。
線程安全實現的方法
線程安全是通過代碼編寫和虛擬機提供同步和鎖機制來實現,相對更偏重後者一些 ,因爲了解了虛擬機線程安全手段的運作過程,自己編寫安全代碼就會不那麼困難。
1、互斥同步
互斥同步是常見的一種併發正確性保障手段。同步是指多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用。而互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥手段。互斥是因,同步是果;互斥是方法,同步是目的。
Java中最基本的同步互斥手段是synchronized關鍵字,經過編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個字節碼指令。這兩個字節碼指令需要一個reference類型的參數來指明要鎖定和解鎖的對象。如果java程序中明確指明瞭這個對象,那就是這個對象的reference,如果沒有明確執行,那就根據synchronized修飾的是實例方法還是類方法,去取這個對象實例或Class對象作爲所對象。
除了synchronized(支持同一個對象可重入)外,我們還可以使用java.util.concurrent的重入鎖(ReentrantLock)來實現同步,基本用法相似,一個表現爲API層面的互斥鎖(lock()和unlock()方法配合try/finally語句塊來執行),另一個表現爲原生語法層面上的互斥鎖。
不過ReentrantLock增加了一些高級功能,主要三項:
等待可中斷:持有鎖的線程長期不釋放鎖,等待的線程可以選擇放棄等待。
可實現公平鎖:通過(非默認)設置,當多個線程等待同一個鎖時,可以按照申請鎖的先後時間依次獲得鎖。而synchronized則不能。
鎖可以綁定多個條件:一個ReentrantLock對象可以同時綁定多個condition對象,只需多次調用newCondition()方法,而synchronized中,鎖對象的wait和notify或notifyall(這些方法會釋放鎖)方法可以實現一個隱含條件,如果要多於一個隱含條件關聯,則不得不額外加一個鎖。
不過,書中作者給出的建議是使用原生的synchronized,因爲後面虛擬機改進都是偏向於synchronized。
2、非阻塞同步
互斥同步主要問題是進行線程阻塞和喚醒所帶來的性能問題,這種同步也稱爲阻塞同步。互斥同步屬於一種悲觀的併發策略,總是認爲如果不去做同步措施就會出問題,無論共享數據是否會真的出現競爭。
但是,隨着硬件指令集的發展(需要操作和衝突檢測這兩個步驟具備原子性,不能靠互斥同步,否則沒有意義),基於衝突檢測的樂觀併發策略:先進行操作,如果沒有其他線程爭用共享數據,那就操作成功,如果出現共享數據爭用,產生衝突,那就採取其他措施。這種樂觀併發策略許多實現並不需要把線程掛起,因此這種同步操作稱爲非阻塞同步。
3、無同步方案
保證線程安全並不一定進行同步,兩者沒有因果關係。同步只是保證共享數據爭用時的正確性手段,如果一個方法本來不共享數據,因此無需任何同步措施,本身就是線程安全。
主要兩類:
可重入代碼:也叫純代碼,可以在任何時刻中斷它,執行別的代碼或調用自身,控制權返回後,原來的程序不會出現任何問題。對於線程安全來說,可重入性是更基本的特性,所有的可重入代碼都是線程安全的,但是並非所有的線程安全代碼都是可重入的。
可重入的代碼有一些特性:不依賴存儲在堆上的數據和公共的系統資源,用到的狀態量都是參數中傳入、不調用非可重入的方法。
判斷是否可重入性:如果一個方法返回的結果是可以預測的,只要輸入相同的數據,就能返回相同的結果,那她就滿足可重入性的要求。
線程本地存儲:如果一點代碼中所需要的數據必須與其他代碼共享,如果能保證這些共享數據的代碼都在同一個線程中執行,那麼就是可把共享數據限制在線程之內,無需同步。
比如 消費者和生產者模式,消耗過程儘量在一個線程內完成。web交互中的一個請求對應一個服務器線程。
鎖優化
1、自旋鎖與自適應自旋
互斥同步線程阻塞對性能影響很大,掛起與恢復轉入內核態完成,給系統併發帶來很大壓力。如果物理機有一個以上的處理器,可以使兩個或以上的線程同時並行執行,那就可以使請求鎖的線程“稍等一會”但不放棄處理器執行時間(空佔着處理器),看看持有鎖的線程是否很快會釋放鎖。爲了讓線程等待,我們只需要線程執行一個忙循環(自旋),這項技術就是所謂的自旋(自旋有一定的限度,否則浪費處理器資源)。因此jdk1.6引入自適應自旋鎖,自選時間不固定,而是有前一次在同一個鎖上的自旋時間及鎖的擁有狀態決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行,那麼虛擬機就認爲這次自旋很有可能再次成功,進而允許自旋等待更長時間。而對於很少成功獲得過鎖的,則等待獲得這個鎖時可能省略自旋過程,進行掛起。
2、鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上的要求同步,但被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除主要判斷依據來自逃逸分析。StringBuffer.append();方法就是同步的,但是調用時根據判斷來決定是否去除鎖。
3、鎖粗化
原則上我們在編寫代碼時,儘量將同步塊縮小,但如果對一個對象反覆加鎖和解鎖,甚至出現在循環體中,即時沒有線程競爭,頻繁互斥操作也會也會帶來很大的性能消耗。
4、輕量級鎖
本意是在沒有多線程競爭的情況下,減少傳統重量級鎖說用系統互斥量產生的性能消耗,並不是替代重量級鎖。用虛擬機中對象頭來操作。在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(01標誌),虛擬機首先將在當前線程的棧幀中建立一個鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝。然後虛擬機使用CAS操作嘗試將對象的Mark Word更新指向Lock Record。如果操作成功了,那這個線程就擁有了該對象的鎖。並且對象的Mark Word標誌位設置爲“00”,表示輕量級鎖。如果更新失敗,看是否指向當前線程的棧幀,如果是則說明已經擁有對象鎖,直接進入同步塊執行,否則說明被其他線程搶佔了。
5、偏向鎖
jdk1.6中引入,消除數據在無競爭情況下的同步原語,進一步提交程序的運行性能。如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步去掉,連CAS操作都不做了。
偏向於第一個獲得它的線程,在接下來的執行過程中,如果鎖沒有被其它線程獲取,則持有偏向鎖的線程永遠不需要在進行同步。
當鎖對象第一次被線程獲取的時候,虛擬機將對象投的標誌位設爲“01”,偏向模式,同時使用CAS把獲取到這個鎖的線程ID記錄在對象那個mark word中,如果操作成功,那麼持有偏向鎖的線程以後每次進入同步代碼塊都不要任何同步操作,如果加鎖解鎖和更新Mark Word等。但是一旦別的線程嘗試獲得這個鎖,則偏向模式結束。根據鎖對象目前是否處於被鎖定狀態,撤銷偏向後恢復到未鎖定(01)或者輕量級鎖(00)。
偏向鎖可以提高帶有同步但無競爭的程序性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章