深入研究 Java Synchronize 和 Lock 的區別與用法
在分散式開發中,鎖是執行緒控制的重要途徑。Java為此也提供了2種鎖機制,synchronized和lock。做為Java愛好者,自然少不了對比一下這2種機制,也能從中學到些分散式開發需要注意的地方。
我們先從最簡單的入手,逐步分析這2種的區別。
一、synchronized和lock的用法區別
synchronized:在需要同步的物件中加入此控制,synchronized可以加在方法上,也可以加在特定程式碼塊中,括號中表示需要鎖的物件。
lock:需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做為鎖,多個執行緒中必須要使用一個ReentrantLock類做為物件才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。所以一般會在finally塊中寫unlock()以防死鎖。
用法區別比較簡單,這裡不贅述了,如果不懂的可以看看Java基本語法。
二、synchronized和lock效能區別
synchronized是託管給JVM執行的,而lock是java寫的控制鎖的程式碼。在Java1.5中,synchronize是效能低效的。因為這是一個重量級操作,需要呼叫操作介面,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock物件,效能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在Java1.6上synchronize的效能並不比Lock差。官方也表示,他們也更支援synchronize,在未來的版本中還有優化餘地。
說到這裡,還是想提一下這2中機制的具體區別。據我所知,synchronized原始採用的是CPU悲觀鎖機制,即執行緒獲得的是獨佔鎖。獨佔鎖意味著其他執行緒只能依靠阻塞來等待執行緒釋放鎖。而在CPU轉換執行緒阻塞時會引起執行緒上下文切換,當有很多執行緒競爭鎖的時候,會引起CPU頻繁的上下文切換導致效率很低。
而Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。樂觀鎖實現的機制就是CAS操作(Compare and Swap)。我們可以進一步研究ReentrantLock的原始碼,會發現其中比較重要的獲得鎖的一個方法是compareAndSetState。這裡其實就是呼叫的CPU提供的特殊指令。
現代的CPU提供了指令,可以自動更新共享資料,而且能夠檢測到其他執行緒的干擾,而 compareAndSet() 就用這些代替了鎖定。這個演算法稱作非阻塞演算法,意思是一個執行緒的失敗或者掛起不應該影響其他執行緒的失敗或掛起的演算法。
我也只是瞭解到這一步,具體到CPU的演算法如果感興趣的讀者還可以在查閱下,如果有更好的解釋也可以給我留言,我也學習下。
三、synchronized和lock用途區別
synchronized原語和ReentrantLock在一般情況下沒有什麼區別,但是在非常複雜的同步應用中,請考慮使用ReentrantLock,特別是遇到下面2種需求的時候。
1.某個執行緒在等待一個鎖的控制權的這段時間需要中斷
2.需要分開處理一些wait-notify,ReentrantLock裡面的Condition應用,能夠控制notify哪個執行緒
3.具有公平鎖功能,每個到來的執行緒都將排隊等候
下面細細道來……
先說第一種情況,ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖,這給我們帶來了很大的靈活性。比如:如果A、B2個執行緒去競爭鎖,A執行緒得到了鎖,B執行緒等待,但是A執行緒這個時候實在有太多事情要處理,就是一直不返回,B執行緒可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。這個時候ReentrantLock就提供了2種機制,第一,B執行緒中斷自己(或者別的執行緒中斷它),但是ReentrantLock不去響應,繼續讓B執行緒等待,你再怎麼中斷,我全當耳邊風(synchronized原語就是如此);第二,B執行緒中斷自己(或者別的執行緒中斷它),ReentrantLock處理了這個中斷,並且不再等待這個鎖的到來,完全放棄。(如果你沒有了解java的中斷機制,請參考下相關資料,再回頭看這篇文章,80%的人根本沒有真正理解什麼是java的中斷,呵呵)
這裡來做個試驗,首先搞一個Buffer類,它有讀操作和寫操作,為了不讀到髒資料,寫和讀都需要加鎖,我們先用synchronized原語來加鎖,如下:
1 | public class Buffer { |
2 |
3 | private Object lock; |
4 |
5 | public Buffer() { |
6 | lock = this ; |
7 | } |
8 |
9 | public void write() { |
10 | synchronized (lock) { |
11 | long startTime = System.currentTimeMillis(); |
12 | System.out.println( "開始往這個buff寫入資料…" ); |
13 | for (;;) // 模擬要處理很長時間 |
14 | { |
15 | if (System.currentTimeMillis() |
16 | - startTime > Integer.MAX_VALUE) |
17 | break ; |
18 | } |
19 | System.out.println( "終於寫完了" ); |
20 | } |
21 | } |
22 |
23 | public void read() { |
24 | synchronized (lock) { |
25 | System.out.println( "從這個buff讀資料" ); |
26 | } |
27 | } |
28 | } |
接著,我們來定義2個執行緒,一個執行緒去寫,一個執行緒去讀。
1 | public class Writer extends Thread { |
2 |
|