性能比較
JDK1.5中,synchronized是性能低效的。因為這是一個重量級操作,它對性能最大的影響是阻塞的是實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,這些操作給系統的並發性帶來了很大的壓力。相比之下使用Java提供的Lock對象,性能更高一些。多線程環境下,synchronized的吞吐量下降的非常嚴重,而ReentrankLock(重入鎖; 可重入鎖)則能基本保持在同一個比較穩定的水平上。
到了JDK1.6,發生了變化,對synchronize加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在JDK1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化余地,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。
下面淺析以下兩種鎖機制的底層的實現策略。
1.互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,因而這種同步又稱為阻塞同步,它屬於一種悲觀的並發策略,即線程獲得的是獨占鎖。獨占鎖意味着其他線程只能依靠阻塞來等待線程釋放鎖。而在CPU轉換線程阻塞時會引起線程上下文切換,當有很多線程競爭鎖的時候,會引起CPU頻繁的上下文切換導致效率很低。synchronized采用的便是這種並發策略。
2.隨着指令集的發展,我們有了另一種選擇:基於沖突檢測的樂觀並發策略,通俗地講就是先進性操作,如果沒有其他線程爭用共享數據,那操作就成功了,如果共享數據被爭用,產生了沖突,那就再進行其他的補償措施(最常見的補償措施就是不斷地重拾,直到試成功為止),這種樂觀的並發策略的許多實現都不需要把線程掛起,因此這種同步被稱為非阻塞同步。ReetrantLock采用的便是這種並發策略。
3.在樂觀的並發策略中,需要操作和沖突檢測這兩個步驟具備原子性,它靠硬件指令來保證,這里用的是CAS操作(Compare and Swap)。JDK1.5之后,Java程序才可以使用CAS操作。我們可以進一步研究ReentrantLock的源代碼,會發現其中比較重要的獲得鎖的一個方法是compareAndSetState,這里其實就是調用的CPU提供的特殊指令。現代的CPU提供了指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而compareAndSet() 就用這些代替了鎖定。這個算法稱作非阻塞算法,意思是一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起。
Java 5中引入了注入AutomicInteger、AutomicLong、AutomicReference等特殊的原子性變量類,它們提供的如:compareAndSet()、incrementAndSet()和getAndIncrement()等方法都使用了CAS操作。因此,它們都是由硬件指令來保證的原子方法。
用途比較
基本語法上,ReentrantLock與synchronized很相似,它們都具備一樣的線程重入特性,只是代碼寫法上有點區別而已,一個表現為API層面的互斥鎖(Lock),一個表現為原生語法層面的互斥鎖(synchronized)。ReentrantLock相對synchronized而言還是增加了一些高級功能,主要有以下三項:
1、等待可中斷:當持有鎖的線程長期不釋放鎖時,正在等待的線程可以選擇放棄等待,改為處理其他事情,它對處理執行時間非常上的同步塊很有幫助。而在等待由synchronized產生的互斥鎖時,會一直阻塞,是不能被中斷的。
2、可實現公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序排隊等待,而非公平鎖則不保證這點,在鎖釋放時,任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖時非公平鎖,ReentrantLock默認情況下也是非公平鎖,但可以通過構造方法ReentrantLock(ture)來要求使用公平鎖。
3、鎖可以綁定多個條件:ReentrantLock對象可以同時綁定多個Condition對象(名曰:條件變量或條件隊列),而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含條件,但如果要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock則無需這么做,只需要多次調用newCondition()方法即可。而且我們還可以通過綁定Condition對象來判斷當前線程通知的是哪些線程(即與Condition對象綁定在一起的其他線程)。