Java併發總結

wait()、notify()、notifyAll()

Object是全部類的基類,它有5個方法組成了等待、通知機制的核心:notify()、notifyAll()、wait()、wait(long) 和wait(long,int)。在java中,全部的類都是從Object繼承而來,所以,全部的類都擁有這些共同的方法可供使用。java

wait()

public final void wait()  throws InterruptedException,IllegalMonitorStateException

該方法用來將當前線程置入休眠狀態,直到接到通知或中斷爲止。在調用wait()以前,線程必需要得到對象的對象級別的鎖,即只能在同步方法或同步代碼塊中調用wait()方法。進入wait()方法後,當前線程釋放鎖。在從wait()返回前,線程與其餘線程競爭從新得到鎖。若是調用wait()時,沒有持有適當的鎖,則拋出IllegalMonitorStateException,它是RuntimeException的一個子類,所以不須要try-catch結構。git

notify()

public final native void notify() throws IllegalMonitorStateException

該方法也要在同步方法或同步代碼塊中調用,即在調用前,線程也必需要得到該對象的對象級別鎖,若是調用notify()時沒有持有適當的鎖,也會拋出IllegalMonitorStateException。github

該方法用來通知那些等待該對象的對象鎖的其餘線程。若是有多個線程等待,則線程規劃器任意挑選其中一個wait()狀態得線程來發出通知,並使它們等待獲取該對象的對象鎖。(notify後,當前線程不會立刻釋放對象鎖,wait所在的線程並不能立刻獲取該對象鎖,要等到程序退出synchronized代碼塊後,當前線程纔會釋放鎖,wait所在的線程才能夠得到該對象鎖)。編程

但不驚動其餘一樣等待該對象notify的線程們,當第一個得到了該對象的wait線程運行完畢以後,它會釋放該對象的鎖,此時若是該對象沒有再次使用notify語句,則即使對象已經空閒,其餘wait狀態等待的線程因爲沒有獲得該對象的通知,會繼續阻塞在wait狀態,直到這個對象發出一個notify或notifyAll。數組

wait狀態等待的是被notify而不是鎖。緩存

notifyAll()

public final native void notifyAll() throws IllegalMonitorStateException

該方法與notify()方法的工做方式相同,重要的一點差別:
notifyAll使全部的方法在該對象wait的線程通通退出wait狀態,變成等待獲取該對象上的鎖,一旦該對象鎖被釋放(即notifyAll線程退出同步代碼塊時),它們就會去競爭。若是其中一個線程得到了該對象的鎖,它就會繼續往下執行,在它退出synchronized代碼塊,釋放鎖後,其餘的已經被喚醒的線程將會繼續競爭該鎖,一直進行下去,直到全部被喚醒的線程都執行完畢。安全

public class WaitAndNotify {
        public static void main(String[] args) throws InterruptedException{
            WaitAndNotify wan = new WaitAndNotify();
    //        synchronized(wan){
    //            wan.wait();
    //            System.out.println("wait");
    //        }
            new Thread(new Runnable(){
                public void run(){
                    synchronized(wan){
                        try {
                            wan.wait();
                            System.out.println("wait");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
            new Thread(new Runnable(){
                public void run(){
                    synchronized(wan){
                        try {
                            wan.wait();
                            System.out.println("wait");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
            new Thread(new Runnable(){
                public void run(){
                    synchronized(wan){
                        wan.notifyAll();        //當notify方法時只執行一個wait、而notifyAll方法將執行兩個wait
                        System.out.println("notify");
                    }
                }
            }).start();
        }
    }

線程的狀態及其轉換

clipboard.png

新建

建立以後未啓動服務器

就緒狀態

調用了start方法,不過還未被OS調用,或者正在等待CPU時間片。多線程

運行狀態

正在被OS執行。架構

阻塞狀態

阻塞狀態分三種:

  • 同步阻塞:線程正在等待一個排它鎖。
  • 限期阻塞:調用Thread.sleep()、join()方法等待sleep時間結束或join線程執行完畢。
  • 無限期阻塞:經過wait()方法進入,等待notify()或notifyAll()方法喚醒。

終止狀態

線程執行完畢或出現異常退了run()。

volatile

做用

volatile能夠保證線程的可見性並提供必定的有序性,可是沒法保證原子性。

原理

在JVM底層,volatile是採用內存屏障來實現的。

內存屏障

內存屏障是一個cpu指令,他的做用:

  • 確保一些特定操做執行的順序
  • 影響一些數據的可見性(多是某些指令執行後的結果)。編譯器和cpu在保證輸出結果同樣的狀況下對指令重排序,使性能獲得優化。插入一個內存屏障,至關於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。

內存屏障跟volatile的關係

若是字段是volatile,java內存模型將在寫操做後插入一個寫屏障指令,在讀操做前插入一個讀屏障指令。這就意味着若是你對一個volatile字段進行寫操做,那麼,一旦你完成寫入,任何訪問這個字段的線程都將會獲得最新的值;在你寫入以前,會保證以前發生的事情已經發生,而且任何更新過的數據值都是可見的。

任務執行

串行執行任務:在單個線程中串行地執行各項任務。

顯式地爲任務建立線程:經過爲每一個線程建立一個新的線程來提供服務,從而實現更高的響應性。這種方式存在必定缺陷:

  • 線程生命週期的開銷很是高:線程的建立於銷燬須要消耗計算資源。
  • 資源消耗:活躍的線程會消耗系統資源,尤爲是內存。若是在可用處理器的數量小於可運行的線程數量時,那麼有些線程將會被閒置。
  • 穩定性:在可建立線程的數量上存在一個限制,若是破壞了這些限制,那麼可能會拋出OutOfMemoryError異常。

線程池

Executor框架:

Executor雖然是一個簡單的接口,但它卻爲靈活且強大的異步任務執行框架提供了基礎。它提供了一種標準的方法將任務的提交過程與執行過程解耦開來,應用Runnable來表示任務。同時它還提供了對生命週期的支持。基於生產者-消費者模式,提交任務的操做至關於生產者,執行任務的線程則至關於消費者。

Executor的生命週期

  • Executor的實現一般會建立線程來執行任務,但若是沒法正確關閉Executor,那麼jvm也將沒法關閉。
  • 爲了解決執行服務的生命週期問題,Executor擴展了ExecutorSerivce藉口,添加了一些用戶生命週期管理的方法。
  • ExecutorService的生命週期有三種狀態:運行、關閉和已終止。

    • ExecutorService在初始化建立時爲運行狀態。
    • Shutdown方法將執行平緩的關閉過程:不在接受新的任務,同時等待已經提交的任務執行完成——包括還未開始執行的任務。
    • ShutdownNow方法將執行粗暴的關閉過程:它將嘗試取消全部運行的任務,而且不在啓動隊列中還沒有開始執行的任務。

Callable跟Future

  • Runnable是必定有很大侷限的抽象,它不能返回一個值或拋出一個受檢查的異常。
  • Callable是一種更好的抽象,它認爲主入口點(cell)可以返回一個值,並可能拋出一個異常。
  • Future表示一個任務的生命週期,並提供相應的方法來判斷是否完成或取消。Executor執行的任務有4個生命週期:建立、提交、開始和完成。任務的生命週期只能前進不能後退。Future的get方法的行爲取決於任務的狀態,若是完成,那麼get會當即返回或者拋出一個Exception,若是任務沒有完成,那麼get將阻塞並指導任務完成,若是任務拋出異常,那麼get將該異常封裝爲ExecutionException並從新拋出。
Future的建立方式:
  • ExecutorService的全部submit方法都返回一個Future,從而將一個Runnable或Callable提交給Executor,並獲得一個Futrue用來獲取任務的執行結果或者取消任務。
  • 顯式的爲某個指定的Runnable或Callable實例化一個FutureTask。(FutureTask實現了Runnable,所以能夠將它交給Executor來執行或者直接調用它的run方法)

線程中斷:

Java並無提供任何機制來安全的終止線程,但它提供了中斷,這是一種協做機制,可以使一個線程終止另外一個線程的當前工做。

Thread的中斷方法:

public class Thread{
        public void interrupt(){….}
        public Boolean isInterrupted(){….}
        public static Boolean interrupted(){….}
}

對中斷的正確理解:他並不會真正的中斷一個正在運行的線程,而只是發出中斷請求,而後由線程在下一個合適的時刻中斷本身。

  • Interrupt():中斷目標線程,
  • IsInterrupt():放回目標線程的中斷狀態
  • Interrupted():清除當前線程的中斷狀態,並返回它以前的值,這也是清除中斷狀態的惟一方法。

Thread.sleep()和Object.wait()都會檢查線程什麼時候中斷,而且在發現中斷時提早放回。它們在響應中斷時執行的操做:清除中斷狀態,拋出InterruptedException,表示阻塞操做因爲中斷而提早結束。可以中斷處於阻塞、限期等待、無限期等待等狀態,但不能中斷I/O阻塞跟synchronized鎖阻塞。

在調用interrupted()時返回true,除非像屏蔽這個中斷,否則就必須對它進行處理。若是一個線程的 run() 方法執行一個無限循環,而且沒有執行 sleep() 等會拋出 InterruptedException 的操做,那麼調用線程的 interrupt() 方法就沒法使線程提早結束。
可是調用 interrupt() 方法會設置線程的中斷標記,此時調用 interrupted() 方法會返回 true。所以能夠在循環體中使用 interrupted() 方法來判斷線程是否處於中斷狀態,從而提早結束線程。

經過ExecutorService中斷
  • shutdownNow跟shutdown
經過Future中斷
  • Future擁有一個cancel方法,該方法帶有一個boolean類型參數mayInterruptIfRunning,表示取消操做是否成功,若是mayIterruptIfRunning爲true而且任務當前正在某個線程中執行,那麼這個線程能被中斷,若是mayInterruptIfRunning爲false,那麼意味着,若任務尚未啓動,就不要執行它。

線程池

線程池:管理一組同構工做線程的資源池,經過重用現有的線程而不是建立新線程,能夠在處理多個請求時分攤在線程建立和銷燬過程當中產生巨大開銷。

類庫提供了一個靈活的線程池以及一些有用的默認配置。能夠用過調用Executors中的靜態工廠方法之一來建立一個線程池:

  • newFixedThreadPool: 建立一個固定長度的線程池,每當提交一個任務時就建立一個線程,直到達到線程池的最大數量。newFixedThreadPool工廠方法將線程池的基本大小和最大大小設置爲參數中的值,而且建立的線程池不會超時。
  • newCachedThreadPool: 建立一個可緩存的線程池,若是線程池的當前規模超過了處理需求時,那麼將回收空閒的線程,而當需求添加時,則能夠添加新的線程,線程池的規模不存在任何限制。newCacheThreadPool工廠方法將線程池的最大大小設置爲Integer.MAX_VALUE,而將基本大小設置爲0,並將超時大小設置爲1分鐘。
  • newSingleThreadPool: 一個單線程Executor,建立單個工做線程來執行任務,若是這個線程異常結束,會建立另外一個線程來替代。newSingleThreadPool可以確保依照任務在隊列中的順序來串行執行(例如FIFO、LIFO、優先級)

ThreadPoolExecutor

ThreadPoolExecutor爲一些Executor提供了基本實現,這些Executor時由Executors中的newFixedThreadPool、newCachedThreadPool和newScheduledThreadExecutor等工廠方法返回的。

ThreadPoolExecutor是一個靈活的、穩定的線程池,容許進行各類定製。若是默認的執行策略不能知足需求,那麼能夠經過ThreadPoolExecutor的構造參數來實例化一個對象,並根據本身的需求來定製。

public ThreadPoolExecutor( int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler){…}

線程池的基本大小,最大大小以及存活時間等因素公共負責線程的建立和銷燬。

  • 基本大小(corePoolSize):線程池的目標大小,即在沒有任務執行時線程池的大小,而且只有 在工做隊列滿了的狀況下才會建立超過這個數量的線程。
  • 最大大小(maximumPoolSize):表示可同時活動的線程數量的上限。
  • 存活時間(keepAliveTime):當某個線程的空閒時間超過了存活時間,那麼將被標記爲可回收的,而且當線程的當前大小超過了基本大小時,這個線程將被終止。

管理隊列任務

在線程池中,若是新請求的到達速率超過線程池的處理速率,那麼新到來的請求將積累起來。在線程池中,這些請求會在一個由Executor管理的Runnable隊列中等待。

ThreadPoolExecutor容許提供一個BlockingQueue來保存等待執行的任務。基本的任務排隊方法有三種:有界隊列、無界隊列和同步移交。隊列的選擇與其餘的配置參數有關,例如線程池的大小等。

  • newFixedThreadPool和newSingleThreadPool在默認狀況下使用一個無界的LinkedBlockingQueue。
  • 一個更穩妥的資源管理策略是使用有界隊列,例如ArrayBlockingQueue、有界的LinkedBlockingQueu、PriorityBlockingQueue。經過飽和策略來解決隊列填滿以後,新任務到來的狀況。
  • 對於很是大的或者無界的線程池,能夠經過使用SynchronousQueue來避免任務排隊,以及直接將任務從生產者持戒交給工做者線程。SynchronousQueue並非一個真正的隊列,而是一種在線程之間隊形移交的機制。要將線程一個元素放入SynchronousQueue中,必須有另外一個線程在等待接受。若是沒有線程等待,而且線程池的當前大小小於最大值,那麼ThreadPoolExecutor將建立一個新的線程。不然根據飽和策略,這個任務將被拒絕。使用直接移交更高效。只有線程池時無界或者能夠拒絕任務時,SynchronousQueue纔有實際價值。在newCacheThreadPool工廠方法中就使用了SynchronousQueue。

飽和策略

當有界隊列被填滿以後,飽和策略開始發揮做用。JDK提供了集中不一樣的RejectedExecutionHadler來實現。

  • 終止(AbortPolicy):默認的飽和策略,將策略將拋出未檢查的RejectExecutionException,調用者能夠捕獲這個異常進行處理。
  • 拋棄(DiscardPolicy):將悄悄拋棄該任務。
  • 拋棄最舊的(DiscardOldestPolicy):拋棄下一個將被執行的任務,而後嘗試從新提交新的任務。(若是工做隊列是一個優先隊列,那麼將拋棄優先級最高的任務)
  • 調用者執行(CallerRunsPolicy):將任務會退給調用者,從而下降任務的流量。它不是在線程池中的線程執行新提交的任務,而是在一個調用了execute的線程執行該任務。在WebServer中使用有界隊列且」調用者運行」飽和策略時,而且線程池全部的線程都被佔用,隊列已滿的狀況下,下一個任務將會由調用execute的主線程執行,此時主線程在執行任務期間不會accept請求,故新到來的請求被保存在TCP層的隊列中而不是應用程序的隊列中,若是持續過載,那麼TCP層的請求隊列最終將被填滿,由於一樣會開始拋出請求。

線程工廠:

每當線程池須要建立一個線程時,都是經過線程工廠方法來完成的,默認的線程工廠方法將建立一個新的、非守護的線程,而且不包含特殊的配置信息。經過制定一個特定的工廠方法,能夠定製線程池的配置信息。在ThreadFactory中只定義了一個方法newThread,每當線程池須要建立一個新線程時都會調用這個方法。

public interface ThreadFactory {
    /**
     * Constructs a new {@code Thread}.  Implementations may also initialize
     * priority, name, daemon status, {@code ThreadGroup}, etc.
     *
     * @param r a runnable to be executed by new thread instance
     * @return constructed thread, or {@code null} if the request to
     *         create a thread is rejected
     */
    Thread newThread(Runnable r);
}

顯式鎖

Java5.0增長了一種新的機制:ReentranLock。與內置加鎖機制不一樣的時,Lock提供了一種無條件的、可輪詢的、定時的以及可中斷的鎖獲取操做,全部加鎖和解鎖的方法都是顯式的。

Lock lock = new ReentrantLock();
        //...
        lock.lock();
        try {
            //更新對象狀態
            //捕獲異常,並在必要時恢復不變形條件
        } finally {
            lock.unlock();  //不會自動清除鎖
        }

當程序的執行控制單元離開被保護的代碼塊時,並不會自動清除鎖。

輪詢鎖與定時鎖

可定時的與可輪詢的鎖獲取模式是由tryLock方法實現的,與無條件的鎖獲取模式相比,它具備更完善的錯誤恢復機制。在內置鎖中,死鎖是一個很嚴重的問題,恢復程序的惟一方式是從新啓動程序,而防止死鎖的惟一方法就是在構造程序時避免出現不一致的鎖順序。可定時的與可輪詢的鎖提供了另外一種選擇:避免死鎖的發生。

若是不能得到全部須要的鎖,那麼可使用可定時的或可輪詢的鎖獲取方式,從而使你從新得到控制權,它會釋放已經得到的鎖,而後從新嘗試獲取全部鎖。(或其餘操做)

在實現具備時間限制的操做時,定時鎖一樣費用有用:當在帶有時間限制的操做中調用一個阻塞方法時,它能根據剩餘時間來提供一個時限,若是不能在制定的時間內給出結果,那麼就會使程序提早結束。

可中斷鎖

內置鎖是不可中斷的,故有時將使得實現可取消得任務變得複雜,而顯示鎖能夠中斷,lockInterruptibly方法可以得到鎖的同時保持對中斷的響應。LockInterruptibly優先考慮響應中斷,而不是響應鎖的普通獲取或重入獲取,既容許線程在還等待時就中斷。(lock優先考慮獲取鎖,待獲取鎖成功以後才響應中斷)

公平性

在ReentrantLock的構造函數中提供了兩種公平性選擇:建立一個非公平的鎖(默認)或者一個公平的鎖,在公平的鎖上,線程講按照它們發出的請求的順序來獲取鎖,但在非公平鎖上,則容許插隊(當一個線程請求非公平鎖時,同時該鎖的狀態變爲可用,那麼這個線程將跳過隊列中全部的等待線程並得到這個鎖)。

在公平鎖中,若是有一個線程持有這個鎖或者有其餘線程在隊列中等待這個鎖,那麼新發的請求的線程將會放入隊列中,而在非公平鎖中,只有當鎖被某個線程持有時,新發出的請求的線程纔會放入隊列中。

大多數狀況下,非公平鎖的性能要高於公平鎖

  • 公平性鎖在線程的掛起以及恢復中須要必定開銷
  • 假設線程A持有一個鎖,而且線程B請求這個鎖,那麼在A釋放時,講喚醒B,這時C也請求這個鎖,那麼可能C極可能會在B被徹底喚醒以前就執行完了。

java內存模型

Java虛擬機規範試圖定義一種java內存模型(Java Memory Model,JMM)來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。

主內存和工做內存

計算機系統中處理器上的寄存器的讀寫速度比內存要快幾個數量級別,爲了解決這種矛盾,在它們之間加入高速緩存。

加入高速緩存的存儲交互很好的解決了處理器與內存的速度矛盾,可是也爲計算機系統帶來了更高的複雜度,由於它引入了一個新的問題:緩存一致性。

多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存,當多個處理器的運算任務涉及到同一塊主內存區域時,將可能致使各自的緩存不一致。

爲了解決一致性問題,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時根據協議來進行操做。

clipboard.png

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存以及從內存中取出變量的底層細節。

JMM將全部變量(實例字段、靜態字段、數組對象的元素,線程共享的)都存儲到主內存中,每一個線程有本身的工做內存,工做內存中保存了被線程使用到的變量的主內存的副本拷貝,線程對變量的全部操做都必須在工做內存中進行。

clipboard.png

內存間的交互操做

Java內存模型定義了8個操做來完成主內存和工做內存間的交互操做。

clipboard.png

  • read:把一個變量從主內存傳輸到工做內存。
  • load:在read以後執行,把read獲得的值放入工做內存的變量副本中。
  • use:把工做內存中的一個變量的值傳遞給執行引擎。
  • assign:把一個從執行引擎接收到的值賦給工做內存的變量。
  • store:把工做內存的一個變量傳送到主內存中。
  • write:在store以後執行,把store獲得的值放入主內存的變量中。
  • lock:做用與主內存變量,它把一個變量標識爲一條線程獨佔的狀態。
  • unlock:把處於鎖定狀態得變量釋放出來。

內存模型的三大特性

原子性

Java內存模型保證了read、load、use、assign、store、write、lock和unlock操做具備原子性,例如對一個int類型的變量執行assign複製操做,這個操做就是原子性的。可是Java內存模型容許虛擬機將沒有被volatile修飾的64位數據(long,double)的讀寫操做劃分爲兩個32位的操做來進行,即load、store、read和write操做能夠不具有原子性。

雖然Java內存模型保證了這些操做的原子性,可是int等原子性操做在多線程中仍是會出現線程安全性問題。
可經過兩個方式來解決:

  • 使用AtomicInteger
  • 經過synchronized互斥鎖來保證操做的原子性。

可見性

可見性指一個線程修改共享變量的值,其餘線程可以當即得知這個修改。Java內存模型經過在變量修改後將新值同步回主內存中,在變量讀取前從主內存刷新變量值來實現可見性。

主要有三種方式實現可見性:

  • volatile
  • synchronized,對一個變量執行unlock操做以前,必須把變量值同步回主內存。
  • final:被final修飾的字段在構造器中一旦初始化完成,而且沒有發生this逃逸(其餘線程有可能經過這個引用訪問到初始化了一半的對象),那麼其餘線程就可以看見final字段的值。

有序性

有序性是指:在本線程內觀察,全部操做都是有序的。在一個線程觀察另外一個線程,全部操做都是無序的,無序是由於發生了指令重排序。在Java內存模型中,容許編譯器和處理器對指令進行重排序,重排序過程當中不會影響到單線程程序的執行,卻會影響多線程併發執行的正確性。

volatile關鍵字經過添加內存屏障的方式禁止重排序,即重排序時不能把後面的指令放在內存屏障以前。

也能夠經過synchronized來保證有序性,它保證每一個時刻只有一個線程執行同步代碼,至關於讓線程順序執行同步代碼。

線程安全的方法

互斥同步

synchronized和ReentranLock。、

非阻塞同步

互斥同步最主要的問題是線程阻塞和喚醒所帶來的性能問題,所以這種這種同步燁稱爲阻塞同步。

互斥同步是一種悲觀的併發策略,老是覺得只要不去作正確的同步策略,那就確定會出問題,不管共享數據是否真的會出現競爭,它都須要進行枷鎖。

CAS

隨着硬件指令集的發展,咱們可使用基於衝突檢測的樂觀併發策略:先進行操做,若是沒有其餘線程競爭共享資源,那麼操做就成功了,不然採起補償措施(不斷嘗試、知道成功爲止)。這種樂觀的併發策略的許多實現都不須要將線程阻塞,所以這種同步操做稱爲非阻塞同步。

樂觀鎖須要操做和衝突檢測這兩個步驟具有原子性,這裏就不能再使用互斥同步來保證,只能靠硬件來完成。硬件支持的原子性操做最典型的是:比較和交換(Compare-And-swap,CAS)。CAS指令須要3個操做數,分別是內存地址V、舊的預期值A和新值B。當操做完成時,只有內存地址V等值舊的預期值時,纔將V值更新爲B。

無同步方案

棧封閉

多個線程訪問同一個方法的局部變量時,不會出現線程安全問題,由於局部變量存儲在虛擬機棧中,屬於線程私用。

本地線程存儲(Thead Local Storage)

若是一段代碼中所須要的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行。若是能保證,咱們就能夠把共享數據的可見範圍限制在同一個線程以內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

符合這種特色的應用並很多見,大部分使用消費隊列的架構模式(如「生產者-消費者」模式)都會將產品的消費過程儘可能在一個線程中消費完。其中最重要的一個應用實例就是經典 Web 交互模型中的「一個請求對應一個服務器線程」(Thread-per-Request)的處理方式,這種處理方式的普遍應用使得不少 Web 服務端應用均可以使用線程本地存儲來解決線程安全問題。

可使用 java.lang.ThreadLocal 類來實現線程本地存儲功能。

synchronized的實現

synchronized關鍵字在通過編譯以後,會在同步代碼塊先後分別造成monitorenter和monitorexit兩個字節碼指令。

在執行monitorenter指令時,首先嚐試獲取對象的鎖,若是這個對象沒有被鎖定或者當前線程已經擁有了這個鎖,就把鎖的計算器+1,相應的執行完monitorexit指令時將鎖計算器減1,當計算器爲0時,鎖就被釋放。

鎖優化

指JVM對synchronized的優化。

自旋鎖

互斥同步進入阻塞狀態的開銷都很大,應該儘可能避免,在許多應用中,共享數據的鎖定狀態只會持續很短的一段時間。自旋鎖的思想是讓一個線程在共享數據的鎖執行忙循環(自旋)一段時間,若是在這段時間內可以得到鎖,就能夠避免進入阻塞狀態。

自旋鎖雖然可以避免進入阻塞狀態從而減小開銷,可是它須要進行忙循環操做佔用cpu時間,它只適用於共享數據的鎖定狀態很短的場景。

在JDK1.6中引入了自適應的自旋鎖,自適應意味着自旋次數再也不是固定的,而是由前一次在同一個鎖上的自旋次數及鎖的擁塞者的狀態來決定。

鎖清除

鎖清除是指對被檢測出不存在競爭的共享數據的鎖進行清除。

鎖清除主要經過逃逸分析來支持,若是堆上的共享數據不可能逃逸出去被其餘線程訪問到,那麼就能夠把他們當成私有數據,也就能夠將他們的鎖清除。

對於一些看起來沒有加鎖的代碼,其實隱式的加了不少鎖,例如一下的字符串拼接代碼就隱式加了鎖:

public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}

String是一個不可變的類,編譯器會對String的拼接進行自動優化。在JDK1.5以前會轉化爲StringBuffer對象連續append()操做。

public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

每個append()方法中都有一個同步塊,虛擬機觀察變量sb,很快發現它的動態做用域被限制在concatString方法內部,也就是說,sb的全部引用永遠不會逃逸到contatString()方法以外,其餘線程沒法訪問到它,所以能夠進行鎖清除。

鎖粗化

若是一系列的操做都對同一對象反覆加鎖和解鎖,頻繁的加鎖操做就會致使性能消耗。

上一節的示例代碼中連續的append()方法就屬於這種狀況。若是虛擬機探測到由這樣的一串零碎的操做都是對同一個對象加鎖,將會把鎖的範圍擴展(粗化)到整個操做序列的外部。對於上一節的示例代碼就是擴展到第一個 append() 操做以前直至最後一個 append() 操做以後,這樣只須要加鎖一次就能夠了。

輕量級鎖

輕量級鎖跟偏向鎖是java1.6中引入的,而且規定鎖只能夠升級而不能降級,這就意味着偏向鎖升級成輕量級鎖後不能下降爲偏向鎖,這種策略是爲了提升得到鎖的效率。

Java對象頭一般由兩個部分組成,一個是Mark Word存儲對象的hashCode或者鎖信息,另外一個是Class Metadata Address用於存儲對象類型數據的指針,若是對象是數組,還會有一部分存儲的是數據的長度。

clipboard.png

對象頭中的Mark Word佈局

clipboard.png

輕量級鎖是相對於傳統的重量級鎖而言,它使用CAS操做來避免重量級鎖使用互斥量的開銷。對於大部分的鎖,在整個同步週期內都是不存在晶振的,所以也就不須要使用互斥量進行同步,能夠先採用CAS操做進行同步,若是CAS失敗了再改用互斥量進行同步。

clipboard.png

當嘗試獲取一個鎖對象時,若是鎖對象標記爲0 01,說明鎖對象的鎖未鎖定(unlock)狀態,此時虛擬機在當前線程的虛擬機棧建立Lock Record,而後使用CAS操做將對象的Mark Word更新爲Lock Record指針。若是CAS操做成功了,那麼線程就獲取了該對象上的鎖,而且對象的Mark Word的鎖標記變爲00,表示該對象處於輕量級鎖狀態。

若是CAS操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的虛擬機棧,若是是的話說明當前線程已經擁有了這個鎖對象,那就能夠直接進入同步塊執行,不然說明這個鎖對象已經被其餘線程搶佔了。若是有兩個以上的線程競爭同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖。

輕量級鎖的步驟以下:

  1. 線程1在執行同步代碼塊以前,JVM會先在當前線程的棧幀中建立一個空間用來存儲鎖記錄,而後再把對象頭中的Mark Word複製到該鎖記錄中,官方稱之爲Displaced Mark Word。而後線程嘗試使用CAS將對象頭中的Mark Word 替換爲指向鎖記錄的指針。若是成功,則得到鎖,進入步驟(3)。若是失敗執行步驟(2)
  2. 線程自旋,自旋成功則得到鎖,進入步驟(3)。自旋失敗,則膨脹成爲重量級鎖,並把鎖標誌位變爲10,線程阻塞進入步驟(3)
  3. 鎖的持有線程執行同步代碼,執行完CAS替換Mark Word成功釋放鎖,若是CAS成功則流程結束,CAS失敗執行步驟(4)
  4. CAS執行失敗說明期間有線程嘗試得到鎖並自旋失敗,輕量級鎖升級爲了重量級鎖,此時釋放鎖以後,還要喚醒等待的線程

偏向鎖

偏向鎖的思想是偏向於讓第一個獲取鎖對象的線程,在以後的獲取該鎖就再也不須要進行同步操做,甚至連CAS操做也不須要。

當鎖對象第一次被線程得到的時候,進入偏向狀態,標記爲1 01。同時使用CAS操做將線程ID記錄到Mark Word中,若是CAS操做成功,這個線程之後每次進入這個鎖相關的同步塊就不須要再進行任何同步操做。

當有另外一個線程去嘗試獲取這個鎖對象,偏向狀態就宣告結束,此時偏向取消後恢復到未鎖定狀態或者輕量級鎖狀態。

偏向鎖得到鎖的步驟分爲:

  1. 初始時對象的Mark Word位爲1,表示對象處於可偏向的狀態,而且ThreadId爲0,這是該對象是biasable&unbiased狀態,能夠加上偏向鎖進入(2)。若是一個線程試圖鎖住biasable&biased而且ThreadID不等於本身ID的時候,因爲鎖競爭應該直接進入(4)撤銷偏向鎖。
  2. 線程嘗試用CAS將本身的ThreadID放置到Mark Word中相應的位置,若是CAS操做成功進入到3),不然進入(4)
  3. 進入到這一步表明當前沒有鎖競爭,Object繼續保持biasable狀態,但此時ThreadID已經不爲0了,對象處於biasable&biased狀態
  4. 當線程執行CAS失敗,表示另外一個線程當前正在競爭該對象上的鎖。當到達全局安全點時(cpu沒有正在執行的字節)得到偏向鎖的線程將被掛起,撤銷偏向(偏向位置0),若是這個線程已經死了,則把對象恢復到未鎖定狀態(標誌位改成01),若是線程還活着,則把偏向鎖置0,變成輕量級鎖(標誌位改成00),釋放被阻塞的線程,進入到輕量級鎖的執行路徑中,同時被撤銷偏向鎖的線程繼續往下執行。
  5. 運行同步代碼塊

參考

相關文章
相關標籤/搜索