java中的鎖


java中有哪些鎖

這個問題在我看了一遍<java並發編程>后盡然無法回答,說明自己對於鎖的概念了解的不夠。於是再次翻看了一下書里的內容,突然有點打開腦門的感覺。看來確實是要學習的最好方式是要帶着問題去學,並且解決問題。

在java中鎖主要兩類:內部鎖synchronized和顯示鎖java.util.concurrent.locks.Lock。但細細想這貌似總結的也不太對。應該是由java內置的鎖和concurrent實現的一系列鎖。

為什么這說,因為在java中一切都是對象,而java對每個對象都內置了一個鎖,也可以稱為對象鎖/內部鎖。通過synchronized來完成相關的鎖操作。

而因為synchronized的實現有些缺陷以及並發場景的復雜性,有人開發了一種顯式的鎖,而這些鎖都是由java.util.concurrent.locks.Lock派生出來的。當然目前已經內置到了JDK1.5及之后的版本中。

synchronized

首先來看看用的比較多的synchronized,我的日常工作中大多用的也是它。synchronized是用於為某個代碼塊的提供鎖機制,在java的對象中會隱式的擁有一個鎖,這個鎖被稱為內置鎖(intrinsic)或監視器鎖(monitor locks)。線程在進入被synchronized保護的塊之前自動獲得這個鎖,直到完成代碼后(也可能是異常)自動釋放鎖。內置鎖是互斥的,一個鎖同時只能被一個線程持有,這也就會導致多線程下,鎖被持有后后面的線程會阻塞。正因此實現了對代碼的線程安全保證了原子性。

可重入

既然java內置鎖是互斥的而且后面的線程會導致阻塞,那么如果持有鎖的線程再次進入試圖獲得這個鎖時會如何呢?比如下面的一種情況:

public class BaseClass { public synchronized void do() { System.out.println("is base"); } } public class SonClass extends BaseClass { public synchronized void do() { System.out.println("is son"); super.do(); } } SonClass son = new SonClass(); son.do();

此時派生類的do方法除了會首先會持有一次鎖,然后在調用super.do()的時候又會再一次進入鎖並去持有,如果鎖是互斥的話此時就應該死鎖了。

但結果卻不是這樣的,這是因為內部鎖是具有可重入的特性,也就是鎖實現了一個重入機制,引用計數管理。當線程1持有了對象的鎖a,此時會對鎖a的引用計算加1。然后當線程1再次獲得鎖a時,線程1還是持有鎖a的那么計算會加1。當然每次退出同步塊時會減1,直到為0時釋放鎖。

synchronized的一些特點

修飾代碼的方式

  • 修飾方法
public class BaseClass { public synchronized void do() { System.out.println("is base"); } }

這種就是直接對某個方法進行加鎖,進入這個方法塊時需要獲得鎖。

  • 修飾代碼塊
public class BaseClass { private static Object lock = new Object(); public void do() { synchronized (lock) { System.out.println("is base"); } } }

這里就將鎖的范圍減少到了方法中的部分代碼塊,這對於鎖的靈活性就提高了,畢竟鎖的粒度控制也是鎖的一個關鍵問題。

對象鎖的類型

經常看到一些代碼中對synchronized使用比較特別,看一下如下的代碼:

public class BaseClass { private static Object lock = new Object(); public void do() { synchronized (lock) { } } public synchronized void doVoid() { } public synchronized static void doStaticVoid() { } public static void doStaticVoid() { synchronized (BaseClass.class) { } } }

這里出現了四種情況:修飾代碼塊,修飾了方法,修飾了靜態方法,修飾BaseClass的class對象。那這幾種情況會有什么不同呢?

  • 修飾代碼塊

這種情況下我們創建了一個對象lock,在代碼中使用synchronized(lock)這種形式,它的意思是使用lock這個對象的內置鎖。這種情況下就將鎖的控制交給了一個對象。當然這種情況還有一種方式:

public void do() { synchronized (this) { System.out.println("is base"); } }

使用this的意思就是當前對象的鎖。這里也道出了內置鎖的關鍵,我提供一把鎖來保護這塊代碼,無論哪個線程來都面對同一把鎖咯。

  • 修飾對象方法

這種直接修飾在方法是咱個情況?其實和修飾代碼塊類似,只不過此時默認使用的是this,也就是當前對象的鎖。這樣寫起代碼來倒也比較簡單明確。前面說過了與修飾代碼塊的區別主要還是控制粒度的區別。

  • 修飾靜態方法

靜態方法難道有啥不一樣嗎?確實是不一樣的,此時獲取的鎖已經不是this了,而this對象指向的class,也就是類鎖。因為Java中的類信息會加載到方法常量區,全局是唯一的。這其實就提供了一種全局的鎖。

  • 修飾類的Class對象

這種情況其實和修改靜態方法時比較類似,只不過還是一個道理這種方式可以提供更靈活的控制粒度。

小結

通過這幾種情況的分析與理解,其實可以看內置鎖的主要核心理念就是為一塊代碼提供一個可以用於互斥的鎖,起到類似於開關的功能。

java中對內置鎖也提供了一些實現,主要的特點就是java都是對象,而每個對象都有鎖,所以可以根據情況選擇用什么樣的鎖。

java.util.concurrent.locks.Lock

前面看了synchronized,大部分的情況下差不多就夠啦,但是現在系統在並發編程中復雜性是越來越高,所以總是有許多場景synchronized處理起來會比較費勁。或者像<java並發編程>中說的那樣,concurrent中的lock是對內部鎖的一種補充,提供了更多的一些高級特性。

java.util.concurrent.locks.Lock簡單分析

這個接口抽象了鎖的主要操作,也因此讓從Lock派生的鎖具備了這些基本的特性:無條件的、可輪循的、定時的、可中斷的。而且加鎖與解鎖的操作都是顯式進行。下面是它的代碼:

public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }

ReentrantLock

ReentrantLock就是可重入鎖,連名字都這么顯式。ReentrantLock提供了和synchronized類似的語義,但是ReentrantLock必須顯式的調用,比如:

public class BaseClass { private Lock lock = new ReentrantLock(); public void do() { lock.lock(); try { //.... } finally { lock.unlock(); } } }

這種方式對於代碼閱讀來說還是比較清楚的,只不過有個問題,就是如果忘了加try finally或忘 了寫lock.unlock()的話導致鎖沒釋放,很有可能導致一些死鎖的情況,synchronized就沒有這個風險。

  • trylock

ReentrantLock是實現Lock接口,所以自然就擁有它的那些特性,其中就有trylock。trylock就是嘗試獲取鎖,如果鎖已經被其他線程占用那么立即返回false,如果沒有那么應該占用它並返回true,表示拿到鎖啦。

另一個trylock方法里帶了參數,這個方法的作用是指定一個時間,表示在這個時間內一直嘗試去獲得鎖,如果到時間還沒有拿到就放棄。

因為trylock對鎖並不是一直阻塞等待的,所以可以更多的規避死鎖的發生。

  • lockInterruptibly

lockInterruptibly是在線程獲取鎖時優先響應中斷,如果檢測到中斷拋出中斷異常由上層代碼去處理。這種情況下就為一種輪循的鎖提供了退出機制。為了更好理解可中斷的鎖操作,寫了一個demo來理解。

package com.test; import java.util.Date; import java.util.concurrent.locks.ReentrantLock; public class TestLockInterruptibly { static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { doPrint("thread 1 get lock."); do123(); doPrint("thread 1 end."); } catch (InterruptedException e) { doPrint("thread 1 is interrupted."); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { doPrint("thread 2 get lock."); do123(); doPrint("thread 2 end."); } catch (InterruptedException e) { doPrint("thread 2 is interrupted."); } } }); thread1.setName("thread1"); thread2.setName("thread2"); thread1.start(); try { Thread.sleep(100);//等待一會使得thread1會在thread2前面執行 } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); } private static void do123() throws InterruptedException { lock.lockInterruptibly(); doPrint(Thread.currentThread().getName() + " is locked."); try { doPrint(Thread.currentThread().getName() + " doSoming1...."); Thread.sleep(5000);//等待幾秒方便查看線程的先后順序 doPrint(Thread.currentThread().getName() + " doSoming2...."); doPrint(Thread.currentThread().getName() + " is finished."); } finally { lock.unlock(); } } private static void doPrint(String text) { System.out.println((new Date()).toLocaleString() + " : " + text); } }

上面代碼中有兩個線程,thread1比thread2更早啟動,為了能看到拿鎖的過程將上鎖的代碼sleep了5秒鍾,這樣就可以感受到前后兩個線程進入獲取鎖的過程。最終上面的代碼運行結果如下:

2016-9-28 15:12:56 : thread 1 get lock. 2016-9-28 15:12:56 : thread1 is locked. 2016-9-28 15:12:56 : thread1 doSoming1.... 2016-9-28 15:12:56 : thread 2 get lock. 2016-9-28 15:13:01 : thread1 doSoming2.... 2016-9-28 15:13:01 : thread1 is finished. 2016-9-28 15:13:01 : thread1 is unloaded. 2016-9-28 15:13:01 : thread2 is locked. 2016-9-28 15:13:01 : thread2 doSoming1.... 2016-9-28 15:13:01 : thread 1 end. 2016-9-28 15:13:06 : thread2 doSoming2.... 2016-9-28 15:13:06 : thread2 is finished. 2016-9-28 15:13:06 : thread2 is unloaded. 2016-9-28 15:13:06 : thread 2 end.

可以看到,thread1先獲得鎖,一會thread2也來拿鎖,但這個時候thread1已經占用了,所以thread2一直到thread1釋放了鎖后才拿到鎖。

**這段代碼說明lockInterruptibly后面來獲取鎖的線程需要等待前面的鎖釋放了才能獲得鎖。**但這里還沒有體現出可中斷的特點,為此增加一些代碼:

thread2.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //1秒后把線程2中斷 thread2.interrupt();

在thread2啟動后調用一下thread2的中斷方法,好吧,先跑一下代碼看看結果:

2016-9-28 15:16:46 : thread 1 get lock. 2016-9-28 15:16:46 : thread1 is locked. 2016-9-28 15:16:46 : thread1 doSoming1.... 2016-9-28 15:16:46 : thread 2 get lock. 2016-9-28 15:16:47 : thread 2 is interrupted. <--直接就響應了線程中斷 2016-9-28 15:16:51 : thread1 doSoming2.... 2016-9-28 15:16:51 : thread1 is finished. 2016-9-28 15:16:51 : thread1 is unloaded. 2016-9-28 15:16:51 : thread 1 end.

和前面的代碼相比可以發現,thread2正在等待thread1釋放鎖,但是這時thread2自己中斷了,thread2后面的代碼則不會再繼續執行。

ReadWriteLock

顧名思義就是讀寫鎖,這種讀-寫鎖的應用場景可以這樣理解,比如一波數據大部分時候都是提供讀取的,而只有比較少量的寫操作,那么如果用互斥鎖的話就會導致線程間的鎖競爭。如果對於讀取的時候大家都可以讀,一旦要寫入的時候就再將某個資源鎖住。這樣的變化就很好的解決了這個問題,使的讀操作可以提高讀的性能,又不會影響寫的操作。

一個資源可以被多個讀者訪問,或者被一個寫者訪問,兩者不能同時進行。

這是讀寫鎖的抽象接口,定義一個讀鎖和一個寫鎖。

public interface ReadWriteLock {
    /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }

在JDK里有個ReentrantReadWriteLock實現,就是可重入的讀-寫鎖。ReentrantReadWriteLock可以構造為公平的或者非公平的兩種類型。如果在構造時不顯式指定則會默認的創建非公平鎖。在非公平鎖的模式下,線程訪問的順序是不確定的,就是可以闖入;可以由寫者降級為讀者,但是讀者不能升級為寫者。

如果是公平鎖模式,那么選擇權交給等待時間最長的線程,如果一個讀線程獲得鎖,此時一個寫線程請求寫入鎖,那么就不再接收讀鎖的獲取,直到寫入操作完成。

  • 簡單的代碼分析 在ReentrantReadWriteLock里其實維護的是一個sync的鎖,只是看起來語義上像是一個讀鎖和寫鎖。看一下它的構造函數:
public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } //讀鎖的構造函數 protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } //寫鎖的構造函數 protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; }

可以看到實際上讀/寫鎖在構造時都是引用的ReentrantReadWriteLock的sync鎖對象。而這個Sync類是ReentrantReadWriteLock的一個內部類。總之讀/寫鎖都是通過Sync來完成的。它是如何來協作這兩者關系呢?

//讀鎖的加鎖方法 public void lock() { sync.acquireShared(1); } //寫鎖的加鎖方法 public void lock() { sync.acquire(1); }

區別主要是讀鎖獲得的是共享鎖,而寫鎖獲取的是獨占鎖。這里有個點可以提一下,就是ReentrantReadWriteLock為了保證可重入性,共享鎖和獨占鎖都必須支持持有計數和重入數。而ReentrantLock是使用state來存儲的,而state只能存一個整形值,為了兼容兩個鎖的問題,所以將其划分了高16位和低16位分別存共享鎖的線程數量或獨占鎖的線程數量或者重入計數。

其他

寫了一大篇感覺要寫下去篇幅太長了,還有一些比較有用的鎖:

  • CountDownLatch

就是設置一個同時持有的計數器,而調用者調用CountDownLatch的await方法時如果當前的計數器不為0就會阻塞,調用CountDownLatch的release方法可以減少計數,直到計數為0時調用了await的調用者會解除阻塞。

  • Semaphone

信號量是一種通過授權許可的形式,比如設置100個許可證,這樣就可以同時有100個線程同時持有鎖,如果超過這個量后就會返回失敗。

 

 

注:此文章為原創,歡迎轉載,請在文章頁面明顯位置給出此文鏈接!
若您覺得這篇文章還不錯請點擊下右下角的推薦,非常感謝!
http://www.cnblogs.com/5207


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM