併發編程中,鎖是常常須要用到的,今天咱們一塊兒來看下Java中的鎖機制:synchronized和lock。java
鎖的種類挺多,包括:自旋鎖、自旋鎖的其餘種類、阻塞鎖、可重入鎖、讀寫鎖、互斥鎖、悲觀鎖、樂觀鎖、公平鎖、可重入鎖等等,其他就不列出了。咱們這邊重點看以下幾種:可重入鎖、讀寫鎖、可中斷鎖、公平鎖。編程
若是鎖具有可重入性,則稱做爲可重入鎖。synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上代表了鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉好比說,當一個線程執行到method1 的synchronized方法時,而在method1中會調用另一個synchronized方法method2,此時該線程沒必要從新去申請鎖,而是能夠直接執行方法method2。bash
讀寫鎖將對一個資源的訪問分紅了2個鎖,如文件,一個讀鎖和一個寫鎖。正由於有了讀寫鎖,才使得多個線程之間的讀操做不會發生衝突。ReadWriteLock
就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。能夠經過readLock()獲取讀鎖,經過writeLock()獲取寫鎖。微信
可中斷鎖,便可以中斷的鎖。在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。 若是某一線程A正在執行鎖中的代碼,另外一線程B正在等待獲取該鎖,可能因爲等待時間過長,線程B不想等待了,想先處理其餘事情,咱們可讓它中斷本身或者在別的線程中中斷它,這種就是可中斷鎖。數據結構
Lock接口中的lockInterruptibly()方法就體現了Lock的可中斷性。多線程
公平鎖即儘可能以請求鎖的順序來獲取鎖。同時有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最早請求的線程)會得到該鎖,這種就是公平鎖。併發
非公平鎖即沒法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能致使某個或者一些線程永遠獲取不到鎖。框架
synchronized
是非公平鎖,它沒法保證等待的線程獲取鎖的順序。對於ReentrantLock
和ReentrantReadWriteLock
,默認狀況下是非公平鎖,可是能夠設置爲公平鎖。性能
synchronized是Java的關鍵字,當它用來修飾一個方法或者一個代碼塊的時候,可以保證在同一時刻最多隻有一個線程執行該段代碼。簡單總結以下四種用法。測試
對某一代碼塊使用,synchronized後跟括號,括號裏是變量,一次只有一個線程進入該代碼塊。
public int synMethod(int m){
synchronized(m) {
//...
}
}
複製代碼
方法聲明時使用,放在範圍操做符以後,返回類型聲明以前。即一次只能有一個線程進入該方法,其餘線程要想在此時調用該方法,只能排隊等候。
public synchronized void synMethod() {
//...
}
複製代碼
synchronized後面括號裏是一對象,此時線程得到的是對象鎖。
public void test() {
synchronized (this) {
//...
}
}
複製代碼
synchronized後面括號裏是類,若是線程進入,則線程在該類中全部操做不能進行,包括靜態變量和靜態方法,對於含有靜態方法和靜態變量的代碼塊的同步,一般使用這種方式。
Lock接口主要相關的類和接口以下。
ReadWriteLock是讀寫鎖接口,其實現類爲ReetrantReadWriteLock。ReetrantLock實現了Lock接口。
Lock中有以下方法:
public interface 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塊中進行,以保證鎖必定被被釋放,防止死鎖的發生。
lockInterruptibly:經過這個方法去獲取鎖時,若是線程正在等待獲取鎖,則這個線程可以響應中斷,即中斷線程的等待狀態。
tryLock:tryLock方法是有返回值的,它表示用來嘗試獲取鎖,若是獲取成功,則返回true,若是獲取失敗(即鎖已被其餘線程獲取),則返回false,也就說這個方法不管如何都會當即返回。在拿不到鎖時不會一直在那等待。
tryLock(long,TimeUnit):與tryLock相似,只不過是有等待時間,在等待時間內獲取到鎖返回true,超時返回false。
unlock:釋放鎖,必定要在finally塊中釋放
實現了Lock接口,可重入鎖,內部定義了公平鎖與非公平鎖。默認爲非公平鎖:
public ReentrantLock() {
sync = new NonfairSync();
}
複製代碼
能夠手動設置爲公平鎖:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製代碼
public interface ReadWriteLock {
Lock readLock(); //獲取讀鎖
Lock writeLock(); //獲取寫鎖
}
複製代碼
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操做分開,分紅2個鎖來分配給線程,從而使得多個線程能夠同時進行讀操做。ReentrantReadWirteLock實現了ReadWirteLock接口,並未實現Lock接口。 不過要注意的是:
若是有一個線程已經佔用了讀鎖,則此時其餘線程若是要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
若是有一個線程已經佔用了寫鎖,則此時其餘線程若是申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
ReetrantReadWriteLock一樣支持公平性選擇,支持重進入,鎖降級。
public class RWLock {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
static Lock r = rwLock.readLock();
static Lock w = rwLock.writeLock();
//讀
public static final Object get(String key){
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
//寫
public static final Object put(String key, Object value){
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
}
複製代碼
只需在讀操做時獲取讀鎖,寫操做時獲取寫鎖。當寫鎖被獲取時,後續的讀寫操做都會被阻塞,寫鎖釋放後,全部操做繼續執行。
下面對synchronized與Lock進行性能測試,分別開啓10個線程,每一個線程計數到1000000,統計兩種鎖同步所花費的時間。網上也能找到這樣的例子。
public class TestAtomicIntegerLock {
private static int synValue;
public static void main(String[] args) {
int threadNum = 10;
int maxValue = 1000000;
testSync(threadNum, maxValue);
testLocck(threadNum, maxValue);
}
//test synchronized
public static void testSync(int threadNum, int maxValue) {
Thread[] t = new Thread[threadNum];
Long begin = System.nanoTime();
for (int i = 0; i < threadNum; i++) {
Lock locks = new ReentrantLock();
synValue = 0;
t[i] = new Thread(() -> {
for (int j = 0; j < maxValue; j++) {
locks.lock();
try {
synValue++;
} finally {
locks.unlock();
}
}
});
}
for (int i = 0; i < threadNum; i++) {
t[i].start();
}
//main線程等待前面開啓的全部線程結束
for (int i = 0; i < threadNum; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("使用lock所花費的時間爲:" + (System.nanoTime() - begin));
}
// test Lock
public static void testLocck(int threadNum, int maxValue) {
int[] lock = new int[0];
Long begin = System.nanoTime();
Thread[] t = new Thread[threadNum];
for (int i = 0; i < threadNum; i++) {
synValue = 0;
t[i] = new Thread(() -> {
for (int j = 0; j < maxValue; j++) {
synchronized(lock) {
++synValue;
}
}
});
}
for (int i = 0; i < threadNum; i++) {
t[i].start();
}
//main線程等待前面開啓的全部線程結束
for (int i = 0; i < threadNum; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("使用synchronized所花費的時間爲:" + (System.nanoTime() - begin));
}
}
複製代碼
測試結果的差別仍是比較明顯的,Lock的性能明顯高於synchronized。本次測試基於JDK1.8。
使用lock所花費的時間爲:436667997
使用synchronized所花費的時間爲:616882878
複製代碼
JDK1.5中,synchronized是性能低效的。由於這是一個重量級操做,它對性能最大的影響是阻塞的是實現,掛起線程和恢復線程的操做都須要轉入內核態中完成,這些操做給系統的併發性帶來了很大的壓力。相比之下使用Java提供的Lock對象,性能更高一些。多線程環境下,synchronized的吞吐量降低的很是嚴重,而ReentrankLock則能基本保持在同一個比較穩定的水平上。
到了JDK1.6,發生了變化,對synchronize加入了不少優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。致使在JDK1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在將來的版本中還有優化餘地,因此仍是提倡在synchronized能實現需求的狀況下,優先考慮使用synchronized來進行同步。
本文主要對併發編程中的鎖機制synchronized和lock,進行詳解。synchronized是基於JVM實現的,內置鎖,Java中的每個對象均可以做爲鎖。對於同步方法,鎖是當前實例對象。對於靜態同步方法,鎖是當前對象的Class對象。對於同步方法塊,鎖是Synchonized括號裏配置的對象。Lock是基於在語言層面實現的鎖,Lock鎖能夠被中斷,支持定時鎖。Lock能夠提升多個線程進行讀操做的效率。經過對比得知,Lock的效率是明顯高於synchronized關鍵字的,通常對於數據結構設計或者框架的設計都傾向於使用Lock而非Synchronized。