其實日常開發中,大多時間沒有使用到lock以及synchronized的場景,導致有些遺忘了,這裏再梳理一下。
1.各自優缺點
synchronized其實是java語言中的一個關鍵字,是內置的特性,這就導致它在使用上有一定的簡潔性,如果使用lock,它作爲接口的實現類,具備更強大的功能,但是需要自己確保鎖的正確釋放,各自優缺點如下:
- 當synchronized塊結束時,會自動釋放鎖,lock一般需要在finally中自己釋放。
- 當synchronized塊執行異常時,也會自動釋放鎖,lock一般需要在finally中自己釋放。
- synchronized使用起來簡單,不需要進行鎖的額外處理,當成關鍵字使用,而lock一般需要創建對象,手動鎖住,手動釋放。
- 當synchronized塊執行阻塞時,例如等待IO或者sleep方法時,無法中斷,並釋放鎖,lock可以在線程休眠時,執行中斷,提高性能。
- 當synchronized塊執行時,只能使用非公平鎖,無法實現公平鎖,而lock可以通過new ReentrantLock(true)設置爲公平鎖,從而在某些場景下提高效率。
- 使用synchronized 無法判斷當前線程是否成功獲取到鎖,lock可以通過trylock進行判斷。
- 另外看資料,synchronized併發是通過阻塞其它線程實現的,線程再進行恢復時,是cpu內核級別的切換,性能較低,後面經過jdk的優化才逐步與lock效率對齊,但是併發場景非常高的情況下,還是lock性能較好。
2.java.util.concurrent.locks包常用類
2.1 Lock
其實是一個接口,定義了幾個方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock方法,其實就是獲取鎖,如果被其它線程獲取,則進行等待。如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:
Lock lock = new ReentrantLock(); lock.lock(); try { //處理任務 } catch (Exception ex) { } finally { lock.unlock(); //釋放鎖 }
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。所以,一般情況下通過tryLock來獲取鎖時是這樣使用的:
Lock lock = new ReentrantLock(); if (lock.tryLock()) { try { //處理任務 } catch (Exception ex) { } finally { lock.unlock(); //釋放鎖 } } else { //如果不能獲取鎖,則直接做其他事情 }
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。一般的使用形式如下:
Lock lock = new ReentrantLock(); lock.lockInterruptibly(); try { //..... } finally { lock.unlock(); }
注意,當一個線程獲取了鎖之後,是不會被interrupt()方法中斷的。因爲本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。
而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
2.2 ReentrantLock
ReentrantLock,意思是“可重入鎖”,ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。
詳見:java.util.concurrent.locks.ReentrantLock ,不再列舉了。
2.3 ReadWriteLock
接口,只定義了兩個方法:
Lock readLock();
Lock writeLock();
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。
2.4 ReentrantReadWriteLock
實現了ReadWriteLock接口。
下面嘗試寫個例子,表示ReadWriteLock和使用synchronized的區別。
使用synchronized時,讀取文件:
public class SynchronizedReadFile {
public static void main(String[] args) {
final SynchronizedReadFile synchronizedReadFile = new SynchronizedReadFile();
new Thread() {
public void run() {
synchronizedReadFile.read(Thread.currentThread());
}
}.start();
new Thread() {
public void run() {
synchronizedReadFile.read(Thread.currentThread());
}
}.start();
}
public synchronized void read(Thread thread) {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在進行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
}
}
結果是 thread0讀取完成,纔開始讀取thread1,其實兩個讀取操作並不要求互斥,可以同時讀取提高效率。
下面是使用reenterantReadWriteLock:
public class ReentrantReadWriteFile {
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public static void main(String[] args) {
final ReentrantReadWriteFile reentrantReadWriteFile = new ReentrantReadWriteFile();
new Thread() {
public void run() {
reentrantReadWriteFile.read(Thread.currentThread());
}
}.start();
new Thread() {
public void run() {
reentrantReadWriteFile.read(Thread.currentThread());
}
}.start();
}
public void read(Thread thread) {
readWriteLock.readLock().lock();
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() + "正在進行讀操作");
}
System.out.println(thread.getName() + "讀操作完畢");
} finally {
readWriteLock.readLock().unlock();
}
}
}
可以看到thread-0 和thread-1是交替同時讀取的,極大的提高了效率。
不過要注意的是:
1.如果有一個線程已經佔用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
2.如果有一個線程已經佔用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
3.下面列了一下鎖的相關的介紹:
1.可重入鎖
如果鎖具備可重入性,則稱作爲可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明瞭鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而由於method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因爲線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。
而由於synchronized和Lock都具備可重入性,所以不會發生上述現象。
2.可中斷鎖
可中斷鎖:顧名思義,就是可以相應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由於等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
lockInterruptibly()的用法時已經體現了Lock的可中斷性。
3.公平鎖
公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
而對於ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置爲公平鎖。
4.讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因爲有了讀寫鎖,才使得多個線程之間的讀操作不會發生衝突。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。