1. 程式人生 > >再入鎖,執行緒安全佇列與執行緒池串想

再入鎖,執行緒安全佇列與執行緒池串想

題中這三者是有一環扣一環的聯絡的,在此做一個總結加深理解。

再入鎖Reentrantlock主要是和synchronized關鍵字作區別,都是加鎖但是排程單位不同。synchronized是以呼叫次數為單位,即被synchronized修飾的方法或者程式碼塊每被執行緒執行一次,都有一個獲取鎖釋放鎖的過程,哪怕是同一個執行緒多次呼叫,所以在遞迴方法中最好不要用synchronized。如上所述,synchronized底層也有獲取鎖和釋放鎖的方法,其獲取鎖和釋放鎖的方法會考慮到物件是否有偏斜鎖,開啟偏斜所即用fast_enter()/fast_exit()方法獲取鎖和釋放鎖。若關閉了偏斜鎖就是slow_enter()/slow_exit()方法。補充一點,偏斜鎖的撤銷操作是很重的開銷,而且偏斜鎖僅僅在synchronized程式碼塊併發度不高的情況下才會體現出效率優勢,很好理解,因為併發高了不斷地撤銷偏斜鎖開銷很大效率當然低了。既然用了synchronized關鍵字說明併發度不低,所以個人感覺偏斜鎖意義不大。

有點扯遠了,再看下再入鎖Reentrantlock,它是以執行緒執行為排程單位的,即如果執行緒已經lock()獲取該物件的鎖,還沒有unlock()釋放掉,那麼當再次嘗試獲取鎖的時候就可以直接獲取成功,即執行緒執行一次在釋放該物件的鎖之前只會獲取一次鎖,這就比較適合遞迴方法中使用了。再入鎖需要注意的是其條件變數condition的應用,這也和後面將要串想的執行緒安全佇列有關。

條件變數是通過再入鎖的newCondition()方法獲得,首先它也是一個物件,有兩個重要的方法await()/signal()。一個執行緒雖然獲取了再入鎖,但是condition.await()方法可以讓該執行緒進入等待狀態,condition.signal()相應的喚醒因同一個條件變數的await()方法進入等待狀態的所有執行緒,這點很重要,和後面將要串想的執行緒安全佇列有關。

再來看執行緒安全佇列,先了解一下基本介紹。JAVA併發包下常用的兩種執行緒安全佇列分別是ConcurrentLinkedQueue和LinkedBlockingQueue。這兩者主要是鎖機制不同。ConcurrentLinkedQueue採用了CAS無鎖機制,而後者就是採用了前文介紹的再入鎖Reentrantlock。準確的說BlockingQueue下所有的執行緒安全佇列都是利用再入鎖機制。當然JDK1.6之後把synchronousQueue的加鎖機制換為CAS機制,synchronousQueue執行緒安全佇列也是newCachedThreadPool()執行緒池的預設佇列,這個暫時先不瞭解,先知道執行緒池的實現離不開執行緒安全佇列即可,後文會詳細介紹。

除synchronousQueue之外,BlockingQueue下其他執行緒安全佇列:

1.ArrayBlockingQueue

它的特點就是是有界的,建立ArrayBlockingQueue要指定容量,還涉及到公平性。

public ArrayBlockingQueue(int Capacity,boolean fair)

2.PriorityBlockingQueue

它的特點是有優先順序概念,雖然是無邊界的,但是畢竟會受到系統資源限制,其實它的邊界是Integer的Max值

3.DelayedQueue和LinkedTransferQueue

這兩個是無邊界的,暫時不瞭解。

4.LinkedBlockingQueue

這個執行緒安全佇列無疑是最重要的,也是無邊界的,是很多執行緒池的預設安全佇列。

其實我感覺BlockingQueue不能稱之為"執行緒安全佇列",因為我理解的執行緒安全加鎖為了避免多執行緒下的資料安全,而BlockingQueue加鎖是為了在佇列非空情況下才讓take(),在佇列沒滿的情況下才讓put()。本質上只是為了在程式設計中省了一個佇列判斷非空判斷長度的過程。通過再入鎖實現的,簡單總結一下原理。

LinkedBlockingQueue的take()和put()用得是再入鎖的兩個不同的條件變數,而ArrayBlockingQueue用的是同一個條件變數,所以LinkedBlockingQueue比ArrayBlockingQueue的鎖顆粒度要細。但是原理大致一樣。在take()方法中有一個while()迴圈去判斷佇列是否為空,如果是空,就notEmpty.await()將該執行緒處於等待狀態,如下:

while(0 == count){
    notEmpty.await();
}
......
notEmpty.signal();

如果不是空,執行緒往下走,take()到值,最後還要通過notEmpty.signal()去喚醒之前因為佇列為空而等待的所有執行緒,這裡是一個很巧妙的設計,可以體會一下,也就是說這些因佇列為空take()造成等待的執行緒是靠一個成功take()到值得執行緒去喚醒的。put()方法也是同理,只不過用notFull條件變數作區分。

前面說的LinkedBlockingQueue的take()和put()用得是再入鎖的兩個不同的條件變數,而ArrayBlockingQueue用的是同一個條件變數,所以LinkedBlockingQueue比ArrayBlockingQueue的鎖顆粒度要細。這裡可以通過大致程式碼對比一下。

//在LinkedBlockingQueue中
//take()中使用的條件變數
private final Reentrantlock takelock = new Reentrantlock();
private final Condition notEmpty = takelock.newCondition();
//put()中使用的條件變數
private final Reentrantlock putlock = new Reentrantlock();
private final Condition notFull = putlock .newCondition();

當然takelock、putlock、notEmpty以及notFull都是定義在take()和put方法外面的 。

//在ArrayBlockingQueue中
//take()和put()中使用同一個條件變數
private final Reentrantlock lock = new Reentrantlock();
private final Condition notEmpty = lock .newCondition();
private final Condition notFull= lock .newCondition();

同上lock、notEmpty以及notFull也是定義在take()和put方法外面的 。

所以,我覺得與其稱之為執行緒安全佇列,不如叫執行緒高效佇列,因為這並不涉及理解中的執行緒安全概念。只是為了take()和put()佇列的時候更加高效,退一萬步講,加鎖操作是在take()和put()方法內加鎖的,是對區域性變數加鎖,區域性變數是執行緒私有的,不存線上程安全問題。如果不加鎖就要有一個判斷佇列是否為空或者判斷長度的過程,省掉了這個過程,如果沒有也是報錯,這也和執行緒安全沒有關係。

再來說說執行緒池,先了解一下執行緒池有三個作用:

1.管理執行緒的建立與銷燬

2.執行緒複用

3.控制執行緒併發量

關於第一點,因為執行緒的建立與銷燬是很重的開銷,將這些交給執行緒池來管理可以提高資源利用率。而第二點和第三點就是利用執行緒池中的工作佇列和threadFactoy來實現的,這裡的工作佇列其實就是一種前面所介紹的執行緒安全佇列,我還是喜歡稱之為執行緒高效佇列,裡面放的是一個個待執行的任務,也可以理解為任務佇列。這麼理解,類實現了Runable介面並重寫了run()方法,這可以理解為一個任務,只有將其start()後才能叫執行緒。用執行緒池管理執行緒其實就是將一個個任務加入任務佇列即工作佇列,將start()啟動任務變為執行緒這個工作交給執行緒池來做。這裡就可以理解為什麼工作佇列要使用執行緒高效隊列了,總不能想啟動的時候找不到任務吧,也不能工作佇列都滿了還往裡面塞任務吧。而重要的是這個threadFactoy,顧名思義執行緒工廠,它是負責建立執行緒去執行工作佇列中的任務的,之所有有第三點可以控制執行緒併發量就是通過控制threadFactoy建立的執行緒數。執行緒創建出來了就會去工作佇列中take()任務,這些邏輯是在一個addwork()方法中實現的,當一個執行緒完成了take()出來的一個任務會再去工作佇列中take(0下一個任務,如果工作佇列為空即沒有任務了,threadFactoy就會銷燬這個執行緒。這就是將執行緒和任務分開,達到了執行緒複用的目的。我們平時寫一個執行緒實現Runable並重寫run()方法建好任務後,通過start()方法啟動執行緒,執行完這個任務這個執行緒也就會銷燬了,沒有複用,而執行緒的建立和銷燬是很重的開銷,所以執行緒池是一個不錯的選擇。不單單執行緒池,物件池,記憶體池以及連線池這些池化技術都值得了解一下。