java中Locks的使用詳解
之前文章中我們講到,java中實現同步的方式是使用synchronized block。在java 5中,Locks被引入了,來提供更加靈活的同步控制。
本文將會深入的講解Lock的使用。
Lock和Synchronized Block的區別
我們在之前的Synchronized Block的文章中講到了使用Synchronized來實現java的同步。既然Synchronized Block那麼好用,為什麼會引入新的Lock呢?
主要有下面幾點區別:
- synchronized block只能寫在一個方法裡面,而Lock的lock()和unlock()可以分別在不同的方法裡面。
- synchronized block 不支援公平鎖,一旦鎖被釋放,任何執行緒都有機會獲取被釋放的鎖。而使用 Lock APIs則可以支援公平鎖。從而讓等待時間最長的執行緒有限執行。
- 使用synchronized block,如果執行緒拿不到鎖,將會被Blocked。 Lock API 提供了一個tryLock() 的方法,可以判斷是否可以獲得lock,這樣可以減少執行緒被阻塞的時間。
- 當執行緒在等待synchronized block鎖的時候,是不能被中斷的。如果使用Lock API,則可以使用 lockInterruptibly()來中斷執行緒。
Lock interface
我們來看下Lock interface的定義,Lock interface定義了下面幾個主要使用的方法:
- void lock() - 嘗試獲取鎖,如果獲取不到鎖,則會進入阻塞狀態。
- void lockInterruptibly() - 和lock()很類似,但是它可以將正在阻塞的執行緒中斷,並丟擲java.lang.InterruptedException。
- boolean tryLock() – 這是lock()的非阻塞版本,它回嘗試獲取鎖,並立刻返回是否獲取成功。
- boolean tryLock(long timeout,TimeUnit timeUnit) – 和tryLock()很像,只是多了一個嘗試獲取鎖的時間。
- void unlock() – unlock例項。
- Condition newCondition() - 生成一個和當前Lock例項繫結的Condition。
在使用Lock的時候,一定要unlocked,以避免死鎖。所以,通常我們我們要在try catch中使用:
Lock lock = ...; lock.lock(); try { // access to the shared resource } finally { lock.unlock(); }
除了Lock介面,還有一個ReadWriteLock介面,在其中定義了兩個方法,實現了讀鎖和寫鎖分離:
- Lock readLock() – 返回讀鎖
- Lock writeLock() – 返回寫鎖
其中讀鎖可以同時被很多執行緒獲得,只要不進行寫操作。寫鎖同時只能被一個執行緒獲取。
接下來,我們幾個Lock的常用是實現類。
ReentrantLock
ReentrantLock是Lock的一個實現,什麼是ReentrantLock(可重入鎖)呢?
簡單點說可重入鎖就是當前執行緒已經獲得了該鎖,如果該執行緒的其他方法在呼叫的時候也需要獲取該鎖,那麼該鎖的lock數量+1,並且允許進入該方法。
不可重入鎖:只判斷這個鎖有沒有被鎖上,只要被鎖上申請鎖的執行緒都會被要求等待。實現簡單
可重入鎖:不僅判斷鎖有沒有被鎖上,還會判斷鎖是誰鎖上的,當就是自己鎖上的時候,那麼他依舊可以再次訪問臨界資源,並把加鎖次數加一。
我們看下怎麼使用ReentrantLock:
public void perform() { lock.lock(); try { counter++; } finally { lock.unlock(); } }
下面是使用tryLock()的例子:
public void performTryLock() throws InterruptedException { boolean isLockAcquired = lock.tryLock(1,TimeUnit.SECONDS); if(isLockAcquired) { try { counter++; } finally { lock.unlock(); } } }
ReentrantReadWriteLock
ReentrantReadWriteLock是ReadWriteLock的一個實現。上面也講到了ReadWriteLock主要有兩個方法:
- Read Lock - 如果沒有執行緒獲得寫鎖,那麼可以多個執行緒獲得讀鎖。
- Write Lock - 如果沒有其他的執行緒獲得讀鎖和寫鎖,那麼只有一個執行緒能夠獲得寫鎖。
我們看下怎麼使用writeLock:
Map<String,String> syncHashMap = new HashMap<>(); ReadWriteLock lock = new ReentrantReadWriteLock(); Lock writeLock = lock.writeLock(); public void put(String key,String value) { try { writeLock.lock(); syncHashMap.put(key,value); } finally { writeLock.unlock(); } } public String remove(String key){ try { writeLock.lock(); return syncHashMap.remove(key); } finally { writeLock.unlock(); } }
再看下怎麼使用readLock:
Lock readLock = lock.readLock(); public String get(String key){ try { readLock.lock(); return syncHashMap.get(key); } finally { readLock.unlock(); } } public boolean containsKey(String key) { try { readLock.lock(); return syncHashMap.containsKey(key); } finally { readLock.unlock(); } }
StampedLock
StampedLock也支援讀寫鎖,獲取鎖的是會返回一個stamp,通過該stamp來進行釋放鎖操作。
上我們講到了如果寫鎖存在的話,讀鎖是無法被獲取的。但有時候我們讀操作並不想進行加鎖操作,這個時候我們就需要使用樂觀讀鎖。
StampedLock中的stamped類似樂觀鎖中的版本的概念,當我們在
StampedLock中呼叫lock方法的時候,就會返回一個stamp,代表鎖當時的狀態,在樂觀讀鎖的使用過程中,在讀取資料之後,我們回去判斷該stamp狀態是否變化,如果變化了就說明該stamp被另外的write執行緒修改了,這說明我們之前的讀是無效的,這個時候我們就需要將樂觀讀鎖升級為讀鎖,來重新獲取資料。
我們舉個例子,先看下write排它鎖的情況:
private double x,y; private final StampedLock sl = new StampedLock(); void move(double deltaX,double deltaY) { // an exclusively locked method long stamp = sl.writeLock(); try { x += deltaX; y += deltaY; } finally { sl.unlockWrite(stamp); } }
再看下樂觀讀鎖的情況:
double distanceFromOrigin() { // A read-only method long stamp = sl.tryOptimisticRead(); double currentX = x,currentY = y; if (!sl.validate(stamp)) { stamp = sl.readLock(); try { currentX = x; currentY = y; } finally { sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); }
上面使用tryOptimisticRead()來嘗試獲取樂觀讀鎖,然後通過sl.validate(stamp)來判斷該stamp是否被改變,如果改變了,說明之前的read是無效的,那麼需要重新來讀取。
最後,StampedLock還提供了一個將read鎖和樂觀讀鎖升級為write鎖的功能:
void moveIfAtOrigin(double newX,double newY) { // upgrade // Could instead start with optimistic,not read mode long stamp = sl.readLock(); try { while (x == 0.0 && y == 0.0) { long ws = sl.tryConvertToWriteLock(stamp); if (ws != 0L) { stamp = ws; x = newX; y = newY; break; } else { sl.unlockRead(stamp); stamp = sl.writeLock(); } } } finally { sl.unlock(stamp); } }
上面的例子是通過使用tryConvertToWriteLock(stamp)來實現升級的。
Conditions
上面講Lock介面的時候有提到其中的一個方法:
Condition newCondition();
Condition提供了await和signal方法,類似於Object中的wait和notify。
不同的是Condition提供了更加細粒度的等待集劃分。我們舉個例子:
public class ConditionUsage { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr,takeptr,count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
上面的例子實現了一個ArrayBlockingQueue,我們可以看到在同一個Lock例項中,建立了兩個Condition,分別代表隊列未滿,佇列未空。通過這種細粒度的劃分,我們可以更好的控制業務邏輯。
本文的例子可以參考https://github.com/ddean2009/learn-java-concurrency/tree/master/Locks
到此這篇關於java中Locks的使用詳解的文章就介紹到這了,更多相關java Locks內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!