ReentrantLock和ReentrantReadWriteLock類的使用

在Java中,可以使用synchronized關鍵字實現線程之間同步互斥,除此以外,JDK中的Lock對象也能實現同步的效果,而且在使用上更加方便靈活,擴展功能也更加強大。常用的兩個Lock類爲ReentrantLock和ReentrantReadWriteLock。

ReentrantLock類在功能上相比synchronized關鍵字更多。

下面先介紹一下ReentrantLock類的使用。

1.ReentrantLock類的使用

(1)調用ReentrantLock對象的lock( )方法獲取鎖,調用unlock( )方法釋放鎖。

在需要進行同步調用的代碼前面添加lock( )方法加上鎖,在同步調用的代碼塊的最後面加unlock( )方法釋放鎖,效果類似於用synchronized關鍵字在同步方法和代碼塊上加鎖。共享同一個對象鎖的線程,它們之間還是順序執行的。

線程會在執行lock( )方法之後獲取鎖,執行unlock( )方法之後釋放鎖。即調用ReentrantLock.lock( )代碼的線程就持有了“對象監視器”,其他線程只有等待鎖被釋放時再次爭搶。

在有try/catch/finall語句塊中的方法中,lock( )方法常放在try語句塊中,unlock( )方法經常放在finally語句塊中。

(2)使用Condition實現等待/通知

關鍵字synchronized與wait( )和notify( )/notityAll( )方法相結合可以實現等待/通知模式,ReentrantLock類也可以實現相同的功能,但需要藉助Condition對象。使用Condition類可以實現更好的靈活性,比如可以實現多路通知功能,也就是在一個Lock裏面可以創建多個Condition(即對象監視器)實例,線程對象可以註冊在指定的Condition中,從而可以有選擇性地進行線程通知,在調度線程上更加靈活。

在使用notify( )/notityAll( )方法進行通知時,通知的線程是由JVM的線程調度程序隨機選擇的。但使用ReentrantLock結合Condition類可以實現“選擇性通知”,這個功能是非常重要的,在Condition類中是默認提供的。

synchronized就相當於整個Lock對象中只有一個單一的Condition對象,所有的線程都註冊在它一個對象的身上。線程執行notify( )方法時,只能隨機喚醒一個等待的線程,不具有靈活性;線程開始notigyAll( )方法時,需要通知所有的WAITING線程,沒有選擇權,會出現相當大的效率問題。

需要注意的是,在調用Condition類實現線程等待時必須在Condition.await( )方法調用之前調用Lock.lock( )代碼獲得同步監視器,否則就會出現監視器錯誤。就像使用wait()和notify( )方法時這兩個方法必須用在被synchronized修飾的同步方法或同步代碼塊中,實現獲取對象監視器,然後再使用。

如果在當前執行任務的線程調用了Condition對象的await( )方法,那麼該線程就會進入等待WAITING狀態。

Object類中的wait( )方法相當於Condition類中的await( )方法。

Object類中的wait(long timeout)方法相當於Condition類中的await(long time,TimeUnit unit)方法。

Object類中的notify( )方法相當於Condition類中的signal( )方法。

Object類中的notifyAll( )方法相當於Condition類中的signalAll( )方法。

(3)使用多個Condition實現等待部分線程

前面使用一個Condition對象實現等待/通知模式,Condition對象也可以創建多個。多個Condition可以實現通知部分線程。

如果想單獨喚醒部分線程,需要使用Condition對象。Condition對象可以喚醒部分指定的線程,有助於提升程序運行的效率。可以先對線程進行分組,然後喚醒指定組中的線程。

使用Condition對象可以喚醒指定種類的線程,這是控制部分線程行爲的方便方式。

(4)用ReentrantLock和Condition實現生產者/消費者模式

關鍵字synchronized與wait( )和notify( )/notityAll( )方法相結合可以實現生產者/消費者模式,ReentrantLock類結合Condition對象也可以實現同樣的效果。和wait( )和notify( )/notityAll( )方法方法一樣,如果是一個生產者和一個消費者能夠很好的運行,但是如果是使用ReentrantLock和Condition對象實現多生產者/多消費者,也有可能出現程序“假死”的情況,這時的解決方法也是把釋放等待線程的signal( )方法變成釋放所有對象的signalAll( )方法,調用signalAll( )方法喚醒的線程還可以根據Condition指定爲特定類型的,就不會出現“假死”的情況了,還會更加靈活。

(5)公平鎖與非公平鎖

鎖Lock分爲“公平鎖”和“非公平鎖”,公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先到先得的FIFO先進先出順序。而非公平鎖就是一種獲取鎖的搶佔機制,是根據線程調度程序的調度隨機獲得鎖。先來的不一定先得到鎖,這個方式可能造成某些線程一直拿不到鎖,結果也就是不公平的了。

這裏需要注意的是:公平鎖只能保證線程執行的基本有序,不能保證線程執行的順序完全按照書寫的順序執行。

非公平鎖的執行基本上是亂序的,先start( )啓動的線程不代表先獲得鎖。

在默認的情況下,ReentrantLock類使用的是非公平鎖。

下面先介紹ReentrantReadWriteLock類的使用。

2.ReentrantReadWriteLock類的使用

ReentrantLock類具有完全互斥排他的效果,即同一時間只能有一個線程在執行ReentrantLock.lock( )方法後面的任務。這樣做雖然保證了實例變量的線程安全性,但效率確實非常低下的。所以在JDK中提供了一種讀寫鎖ReentrantReadWriteLock類,使用它可以加快運行效率,在某些不需要操作實例變量的方法中,完全可以使用讀寫鎖ReentrantReadWriteLock類來提升該方法的代碼運行速度。

讀寫鎖表示有兩個鎖:一個是讀操作相關的鎖,也稱爲共享鎖;另一個是寫操作相關的鎖,也叫排他鎖。在這兩個鎖中規定:多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。在沒有現成Thread進行寫入操作時,進行讀取操作的多個Thread都可以獲取讀鎖,而進行寫入操作的Thread只有在獲取寫鎖後才能進行寫入操作。即多個Thread可以同時進行讀取操作,但是同一時間只允許一個Thread進行寫入操作。

ReentrantLock類的讀寫使用規則簡化來說就是:讀讀共享,寫寫互斥,讀寫互斥,寫讀互斥。

3.總結

在學習併發時,Lock是synchronized關鍵字的進階,掌握Lock有助於學習併發包中源代碼的實現原理,在併發包中大量的類使用Lock接口作爲同步的處理方式,

參考《Java多線程編程核心技術》---高洪巖(Chapter4 4.1 4.2)

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