Java經常使用鎖機制簡介

  在開發Java多線程應用程序中,各個線程之間因爲要共享資源,必須用到鎖機制。Java提供了多種多線程鎖機制的實現方式,常見的有synchronized、ReentrantLock、Semaphore、AtomicInteger等。每種機制都有優缺點與各自的適用場景,必須熟練掌握他們的特色才能在Java多線程應用開發時駕輕就熟。html

  更多Java鎖機制的詳細介紹參見文檔《Java鎖機制詳解》。java

1、synchronized

  幾乎每個Java開發人員都認識synchronized,使用它來實現多線程的同步操做是很是簡單的,只要在須要同步的對方的方法、類或代碼塊中加入該關鍵字,它可以保證在同一個時刻最多隻有一個線程執行同一個對象的同步代碼,可保證修飾的代碼在執行過程當中不會被其餘線程干擾。使用synchronized修飾的代碼具備原子性和可見性,在須要進程同步的程序中使用的頻率很是高,能夠知足通常的進程同步要求(詳見《Java多線程基礎》)。安全

  synchronized實現的機理依賴於軟件層面上的JVM,所以其性能會隨着Java版本的不斷升級而提升。事實上,在Java1.5中,synchronized是一個重量級操做,須要調用操做系統相關接口,性能是低效的,有可能給線程加鎖消耗的時間比有用操做消耗的時間更多。到了Java1.6,synchronized進行了不少的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提升。在以後推出的Java1.7與1.8中,均對該關鍵字的實現機理作了優化。多線程

  須要說明的是,當線程經過synchronized等待鎖時是不能被Thread.interrupt()中斷的,所以程序設計時必須檢查確保合理,不然可能會形成線程死鎖的尷尬境地。併發

  最後,儘管Java實現的鎖機制有不少種,而且有些鎖機制性能也比synchronized高,但仍是強烈推薦在多線程應用程序中使用該關鍵字,由於實現方便,後續工做由JVM來完成,可靠性高。只有在肯定鎖機制是當前多線程程序的性能瓶頸時,才考慮使用其餘機制,如ReentrantLock等。ide

2、ReentrantLock

  可重入鎖,顧名思義,這個鎖能夠被線程屢次重複進入進行獲取操做。ReentantLock繼承接口Lock並實現了接口中定義的方法,除了能完成synchronized所能完成的全部工做外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多線程死鎖的方法。函數

  Lock實現的機理依賴於特殊的CPU指定,能夠認爲不受JVM的約束,並能夠經過其餘語言平臺來完成底層的實現。在併發量較小的多線程應用程序中,ReentrantLock與synchronized性能相差無幾,但在高併發量的條件下,synchronized性能會迅速降低幾十倍,而ReentrantLock的性能卻能依然維持一個水準,所以咱們建議在高併發量狀況下使用ReentrantLock。高併發

  ReentrantLock引入兩個概念:公平鎖非公平鎖公平鎖指的是鎖的分配機制是公平的,一般先對鎖提出獲取請求的線程會先被分配到鎖。反之,JVM按隨機、就近原則分配鎖的機制則稱爲不公平鎖。ReentrantLock在構造函數中提供了是否公平鎖的初始化方式,默認爲非公平鎖。這是由於,非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊須要,不然最經常使用非公平鎖的分配機制。性能

  ReentrantLock經過方法lock()與unlock()來進行加鎖與解鎖操做,與synchronized會被JVM自動解鎖機制不一樣,ReentrantLock加鎖後須要手動進行解鎖。爲了不程序出現異常而沒法正常解鎖的狀況,使用ReentrantLock必須在finally控制塊中進行解鎖操做。一般使用方式以下所示:優化

1 Lock lock = new ReentrantLock();
2 try {
3     lock.lock();
4     //...進行任務操做
5 } finally {
6     lock.unlock();
7 }

  下面咱們詳細介紹有關ReentrantLock提供的可響應中斷鎖可輪詢鎖請求定時鎖等機制與操做方式。

  一、線程在等待資源過程當中須要中斷

  ReentrantLock的在獲取鎖的過程中有2種鎖機制,忽略中斷鎖響應中斷鎖。當等待線程A或其餘線程嘗試中斷線程A時,忽略中斷鎖機制則不會接收中斷,而是繼續處於等待狀態;響應中斷鎖則會處理這個中斷請求,並將線程A由阻塞狀態喚醒爲就緒狀態,再也不請求和等待資源。

  lock.lock()可設置鎖機制爲忽略中斷鎖,lock.lockInterruptibly()可設置鎖機制爲響應中斷鎖。下述例子描述了,一個寫線程和一個讀線程分別操做同一個同一個對象的寫方法和讀方法,寫方法須要執行10秒時間,主線程中在啓動寫線程writer和讀線程reader後,啓動了第三個線程,這個線程判斷當程序執行5秒後,若是讀線程依然處於等待狀態,就將他中斷,再也不繼續等待資源。

 1 import java.util.concurrent.locks.ReentrantLock;
 2 
 3 public class ReentrantLockInterrupt {
 4     public static void main(String[] args) {
 5         MyBuffer buffer = new MyBuffer();
 6         
 7         //開啓寫線程
 8         final WriteThread write = new WriteThread(buffer);
 9         write.start();
10         
11         //開啓讀線程
12         final ReadThread read = new ReadThread(buffer);
13         read.start();
14         
15         //開啓第三個線程,用於監聽並中斷讀線程
16         new Thread(new Runnable() {
17             @Override
18             public void run() {
19                 long readThreadMaxWaitTime = 5000;  //讀線程最大等待時間,單位:毫秒
20                 long startTime = System.currentTimeMillis();
21                 while(System.currentTimeMillis()-startTime<readThreadMaxWaitTime){}
22                 System.out.println("讀線程等待時間已超過"+readThreadMaxWaitTime/1000+"秒,請求中斷....");
23                 read.interrupt();
24             }
25         }).start();
26     }
27 }
28 
29 class WriteThread extends Thread{
30     private MyBuffer buffer;
31     public WriteThread(MyBuffer buffer){
32         this.buffer = buffer;
33     }
34     @Override
35     public void run() {
36         buffer.write();
37     }
38 }
39 
40 class ReadThread extends Thread{
41     private MyBuffer buffer;
42     public ReadThread(MyBuffer buffer) {
43         this.buffer = buffer;
44     }
45     @Override
46     public void run() {
47         try {
48             buffer.read();
49         } catch (InterruptedException e) {
50             System.out.println("讀線程已經被中斷.....");
51         }
52     }
53 }
54 
55 class MyBuffer {
56     //使用ReentrantLock鎖
57     private ReentrantLock lock = new ReentrantLock();
58     
59     //寫操做
60     public void write(){
61         //lock操做必須放在此處,放於try內就會報錯,爲何???
62         lock.lock();
63         try {
64             long writeNeedTime = 10000;  //寫操做須要時間,單位:毫秒
65             long writeStartTime = System.currentTimeMillis();
66             System.out.println("寫操做開始,預計執行時間:"+writeNeedTime/1000+"秒....");
67             while(System.currentTimeMillis()-writeStartTime<writeNeedTime){}
68             System.out.println("寫操做完成....");
69         } finally {
70             lock.unlock();
71         }
72     }
73     
74     //讀操做
75     public void read() throws InterruptedException {
76         //lock()方法設置鎖機制爲「忽略中斷鎖」,當調用此方法的線程自身或被其餘線程請求中斷(interrupt)時,操做線程不響應請求,繼續處於等待狀態
77         //lockInterruptibly()方法可設置鎖機制爲「相應式中斷鎖」,當調用此方法的線程自身或被其餘線程請求中斷(interrupt)時,線程會相應請求,並在調用當前方法的操做時中斷線程,中斷後不操做線程後續任務
78         //以上的響應指的是線程正在獲取鎖的過程當中被請求中斷,若線程在其餘非阻塞與阻塞狀態時被請求中斷,lockInterruptibly()是沒法響應中斷的,
79         //非阻塞狀態可根據中斷標記位Thread.currentThread().isInterrupted(),阻塞狀態可經過拋出異常InterruptedException來中斷線程
80         //詳細能夠參考http://www.cnblogs.com/hanganglin/articles/3517178.html中的Thread.interrupt資料
81         lock.lockInterruptibly();
82         try {
83             System.out.println("讀操做完成....");
84         } finally {
85             lock.unlock();
86         }
87     }
88 }
View Code

  由例子可知,ReentrantLock.lockInterruptibly()方法可設置線程在獲取鎖的時候響應其餘線程對當前線程發出的中斷請求。但必須注意,此處響應中斷鎖是指正在獲取鎖的過程當中,若是線程此時並不是處於獲取鎖的狀態,經過此方法設置是沒法中斷線程的,非阻塞狀態可根據中斷標記位Thread.currentThread().isInterrupted()在程序中手動設置中斷,阻塞狀態可經過拋出異常InterruptedException來中斷線程,詳細可參考博文《Java多線程基礎》。

  二、實現可輪詢的鎖請求

  在synchronized中,一旦發生死鎖,惟一可以恢復的辦法只能從新啓動程序,惟一的預防方法是在設計程序時考慮完善不要出錯。而有了Lock之後,死鎖問題就有了新的預防辦法,它提供了tryLock()輪詢方法來得到鎖,若是鎖可用則獲取鎖,若是鎖不可用,則此方法返回false,並不會爲了等待鎖而阻塞線程,這極大地下降了死鎖狀況的發生。典型使用語句以下:

Lock lock = ...;
if(lock.tryLock()){
    //鎖可用,則成功獲取鎖
    try {
        //獲取鎖後進行處理
    } finally {
        lock.unlock();
    }
} else {
    //鎖不可用,其餘處理方法
}
View Code

  三、定時鎖請求

  在synchronized中,一旦發起鎖請求,該請求就不能中止了,若是不能得到鎖,則當前線程會阻塞並等待得到鎖。在某些狀況下,你可能須要讓線程在必定時間內去得到鎖,若是在指定時間內沒法獲取鎖,則讓線程放棄鎖請求,轉而執行其餘的操做。Lock就提供了定時鎖的機制,使用Lock.tryLock(long timeout, TimeUnit unit)來指定讓線程在timeout單位時間內去爭取鎖資源,若是超過這個時間仍然不能得到鎖,則放棄鎖請求,定時鎖能夠避免線程陷入死鎖的境地。

  在上面的實例一中,其餘線程在5秒後向正在等候鎖的讀線程發起中斷請求,讀線程響應請求併成功中斷。也能夠在讀線程中設置定時鎖,設定在5秒內爭奪鎖,超時則放棄鎖,並結束當前的讀線程,使用定時鎖實現讀方法代碼以下:

public void read() throws InterruptedException{
        //使用定時鎖,若是在5秒內仍然不能得到鎖,則放棄鎖請求
        if(lock.tryLock(5,TimeUnit.SECONDS)){
            try {
                System.out.println("讀操做順利完成,釋放鎖....");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("讀線程在5秒內沒法獲取鎖,放棄請求,結束讀線程工做....");
        }
}
View Code
3、Semaphore

  上述兩種鎖機制類型都是「互斥鎖」,學過操做系統的都知道,互斥是進程同步關係的一種特殊狀況,至關於只存在一個臨界資源,所以同時最多隻能給一個線程提供服務。可是,在實際複雜的多線程應用程序中,可能存在多個臨界資源,這時候咱們能夠藉助Semaphore信號量來完成多個臨界資源的訪問。

  Semaphore基本能完成ReentrantLock的全部工做,使用方法也與之相似,經過acquire()與release()方法來得到和釋放臨界資源。經實測,Semaphone.acquire()方法默認爲可響應中斷鎖,與ReentrantLock.lockInterruptibly()做用效果一致,也就是說在等待臨界資源的過程當中能夠被Thread.interrupt()方法中斷。

  此外,Semaphore也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名tryAcquire與tryLock不一樣,其使用方法與ReentrantLock幾乎一致。Semaphore也提供了公平與非公平鎖的機制,也可在構造函數中進行設定。

  Semaphore的鎖釋放操做也由手動進行,所以與ReentrantLock同樣,爲避免線程因拋出異常而沒法正常釋放鎖的狀況發生,釋放鎖的操做也必須在finally代碼塊中完成

  Semaphore支持多個臨界資源,而ReentrantLock只支持一個臨界資源,筆者認爲ReentrantLock是Semaphore的一種特殊狀況。Semaphore的使用方法與ReentrantLock實在太過類似,在此再也不舉例說明。

4、AtomicInteger

  首先說明,此處AtomicInteger是一系列相同類的表明之一,常見的還有AtomicLong、AtomicLong等,他們的實現原理相同,區別在與運算對象類型的不一樣。使人興奮地,還能夠經過AtomicReference<V>將一個對象的全部操做轉化成原子操做。

  咱們知道,在多線程程序中,諸如++i 或 i++等運算不具備原子性,是不安全的線程操做之一。一般咱們會使用synchronized將該操做變成一個原子操做,但JVM爲此類操做特地提供了一些同步類,使得使用更方便,且使程序運行效率變得更高。經過相關資料顯示,一般AtomicInteger的性能是ReentantLock的好幾倍。

相關文章
相關標籤/搜索