Synchronized與Lock之間的哪些事兒
Synchronized
一、應用
- 對於普通同步方法,鎖是當前例項物件;
- 對於靜態同步方法,鎖是當前類的Class物件;
- 對於同步程式碼塊,鎖是Synchronized括號裡配置的物件。
二、實現原理
當前一個執行緒試圖訪問同步程式碼塊時,它首先必須得到鎖,退出或者丟擲異常時候必須釋放鎖。那麼鎖到底存在什麼地方?
從JVM規範可以看到Synchonized在JVM裡的實現原理,JVM基於進入和退出Monitor物件來實現方法同步和程式碼快同步,但兩者的實現細節不一樣。synchronized關鍵字編譯後會在同步塊的前後新增上montorenter和monitorexit兩個位元組碼指令,monitorenter指令執行時會讓物件的鎖計數加1,而monitorexit指令執行時會讓物件的鎖計數減1,這兩個位元組碼指令都需要一個指向鎖定和解鎖物件的reference,如果指定了同步的物件reference就指向這個物件,如果修飾的是方法,如果是類方法就指向Class物件,如果是例項方法就指向這個例項。
三、java物件頭
鎖存在Java物件頭裡。如果物件是陣列型別,則虛擬機器用3個Word(字寬)儲存物件頭,如果物件是非陣列型別,則用2字寬儲存物件頭。在32位虛擬機器中,一字寬等於四位元組,即32bit。
Java物件頭裡的Mark Word裡預設儲存物件的HashCode,分代年齡和鎖標記位。
在執行期間Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。Mark Word可能變化為儲存以下4種資料:輕量級鎖、重量級鎖、GC標記、偏向鎖
四、鎖的升級與對比
Java SE1.6為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裡鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
五、注意
1)當一個執行緒正在訪問一個物件的synchronized方法,那麼其他執行緒不能訪問該物件的其他synchronized方法。這個原因很簡單,因為一個物件只有一把鎖,當一個執行緒獲取了該物件的鎖之後,其他執行緒無法獲取該物件的鎖,所以無法訪問該物件的其他synchronized方法。
2)當一個執行緒正在訪問一個物件的synchronized方法,那麼其他執行緒能訪問該物件的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該物件的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那麼其他執行緒是可以訪問這個方法的,
3)如果一個執行緒A需要訪問物件object1的synchronized方法fun1,另外一個執行緒B需要訪問物件object2的synchronized方法fun1,即使object1和object2是同一型別),也不會產生執行緒安全問題,因為他們訪問的是不同的物件,所以不存在互斥問題。
4)對於synchronized方法或者synchronized程式碼塊,當出現異常時,JVM會自動釋放當前執行緒佔用的鎖,因此不會由於異常導致出現死鎖現象。
六、缺陷
如果獲取鎖的執行緒由於要等待IO或者其他原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,其他執行緒便只能乾巴巴地等待,試想一下,這多麼影響程式執行效率。
再舉個例子:當有多個執行緒讀寫檔案時,讀操作和寫操作會發生衝突現象,寫操作和寫操作會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。
但是採用synchronized關鍵字來實現同步的話,就會導致一個問題:
如果多個執行緒都只是進行讀操作,所以當一個執行緒在進行讀操作時,其他執行緒只能等待無法進行讀操作。
Lock
一、lock
1)Lock不是Java語言內建的,synchronized是Java語言的關鍵字,因此是內建特性。Lock是一個類(介面),通過這個類可以實現同步訪問;
2)Lock和synchronized有一點非常大的不同,採用synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而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,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。
② tryLock( )
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
③ tryLock( long time, TimeUnit unit )
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
④ lockInterruptibly( )
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。也就使說,當兩個執行緒同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時執行緒A獲取到了鎖,而執行緒B只有在等待,那麼對執行緒B呼叫threadB.interrupt()方法能夠中斷執行緒B的等待過程。
由於lockInterruptibly()的宣告中丟擲了異常,所以lock.lockInterruptibly()必須放在try塊中或者在呼叫lockInterruptibly()的方法外宣告丟擲InterruptedException。
ReentrantLock
可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。像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都具備可重入性,所以不會發生上述現象。
ReentrantLock是唯一實現了Lock介面的類,並且ReentrantLock提供了更多的方法。下面通過一些例項看具體看一下如何使用ReentrantLock。
① lock()
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
Lock lock = new ReentrantLock(); //注意這個地方
lock.lock();
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
}
}
Thread-0得到了鎖
Thread-1得到了鎖
Thread-0釋放了鎖
Thread-1釋放了鎖
怎麼會輸出這個結果?第二個執行緒怎麼會在第一個執行緒釋放鎖之前得到了鎖?原因在於,在insert方法中的lock變數是區域性變數,每個執行緒執行該方法時都會儲存一個副本,那麼理所當然每個執行緒執行到lock.lock()處獲取的是不同的鎖,所以就不會發生衝突。那麼只需要將lock宣告為類的屬性即可。
private Lock lock = new ReentrantLock();
② tryLock()
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意這個地方
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
if(lock.tryLock()) {
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
} else {
System.out.println(thread.getName()+"獲取鎖失敗");
}
}
}
結果
Thread-0得到了鎖
Thread-1獲取鎖失敗
Thread-0釋放了鎖
③ lockInterruptibly()
public class Test {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
Test test = new Test();
MyThread thread1 = new MyThread(test);
MyThread thread2 = new MyThread(test);
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意,如果需要正確中斷等待鎖的執行緒,必須將獲取鎖放在外面,然後將InterruptedException丟擲
try {
System.out.println(thread.getName()+"得到了鎖");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
break;
//插入資料
}
}
finally {
System.out.println(Thread.currentThread().getName()+"執行finally");
lock.unlock();
System.out.println(thread.getName()+"釋放了鎖");
}
}
}
class MyThread extends Thread {
private Test test = null;
public MyThread(Test test) {
this.test = test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中斷");
}
}
}
ReadWriteLock
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();
}
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將檔案的讀寫操作分開,分成2個鎖來分配給執行緒,從而使得多個執行緒可以同時進行讀操作。下面的ReentrantReadWriteLock實現了ReadWriteLock介面。
ReentrantReadWriteLock
ReentrantReadWriteLock裡面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。
下面通過幾個例子來看一下ReentrantReadWriteLock具體用法。
假如有多個執行緒要同時進行讀操作的話,先看一下synchronized達到的效果:
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public synchronized void get(Thread thread) {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在進行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
}
}
這段程式的輸出結果會是,直到thread1執行完讀操作之後,才會列印thread2執行讀操作的資訊。而改成讀寫鎖的話,則會同時進行。
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public void get(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在進行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
} finally {
rwl.readLock().unlock();
}
}
}
不過要注意的是,如果有一個執行緒已經佔用了讀鎖,則此時其他執行緒如果要申請寫鎖,則申請寫鎖的執行緒會一直等待釋放讀鎖。
如果有一個執行緒已經佔用了寫鎖,則此時其他執行緒如果申請寫鎖或者讀鎖,則申請的執行緒會一直等待釋放寫鎖。
Lock和Synchronized的選擇
總結來說,Lock和synchronized有以下幾點不同:
1)Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現;
2)synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個執行緒進行讀操作的效率。
公平鎖
公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些執行緒永遠獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。
而對於ReentrantLock和ReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為公平鎖。
樂觀與悲觀鎖
悲觀鎖:假定會發生併發衝突,遮蔽一切可能違反資料完整性的操作。(s和l都是悲觀鎖)
樂觀鎖:假設不會發生併發衝突,只在提交操作時檢查是否違反資料完整性。
作者:Tim
歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。