淺談Java中鎖的實現和優化

以前有一篇文章咱們簡單的談到了Java中同步的問題,可是可能在日常的開發中,有些理論甚至是某些方式是用不到的,可是從程序的角度看,這些理論思想咱們能夠運用到咱們的開發中,好比是否是應該一談到同步問題,就應該想到用synchronized?,何時應該用ReentrantLock?,是否是應該考慮用原子類解決某些問題?那咱們就來聊一聊咱們如何用這個鎖。安全

上一篇文章中第一個例子,咱們後來經過對方法加synchronized修改了代碼,在這裏還有一種修改方式,咱們能夠考慮使用原子類,這樣也能夠解決這個問題,咱們來看看僞代碼:併發

private static AtomicInteger i = new AtomicInteger(0);

private static void increse(){
        i.incrementAndGet();
    }

由於在這個問題上,不僅是有線程的可見性問題,還有一個就是操做的原子性問題,說到這裏,咱們應該明白,在咱們日常的開發中,有時候應該區分何時是須要原子操做,何時只須要可見性,那麼這樣咱們纔可以正確的判斷用某種合理的方式寫出合理的代碼。app

好,這是上次的一點小問題咱們再這裏重複一下,接下來咱們聊一聊具體的鎖。性能

互斥同步

synchronized就是一個很典型的例子,這種鎖也叫作可重入鎖,就是一個線程持有一個相同的鎖對象,能夠進行重複加解鎖,換句話說,持有相同鎖對象的線程不會本身把本身鎖住。優化

還有另一個和synchronized很相近的鎖,就是咱們上面提到的ReentrantLock,這個和synchronized同樣,都提供了可重入性,這兩個鎖的效果是差很少的(在之前的一些比較舊的JDK版本中,併發數比較大的狀況下,ReentrantLock的性能是要優於synchronized的),可能有些小夥伴尚未用過這種鎖,那這裏咱們就簡單說一下具體用法:ui

ReentrantLock lock = new ReentrantLock();

lock.lock();
// code
lock.unlock();

這裏從API的層面提供了一個比較簡單的寫法,同時也提供了一些比較高級的特性,像信號量,線程終止等等,有興趣的小夥伴能夠去查閱相關資料去了解一下,在這裏咱們不深刻探討,可是有一個地方須要注意一下,就是ReentrantLock提供了兩種競爭策略,一種是公平策略,另外一種是非公平策略。編碼

ReentrantLock lock = new ReentrantLock();// 非公平策略

ReentrantLock lock = new ReentrantLock(true);// 公平策略
  • 公平策略與非公平策略

這裏我就先舉一個比較簡單的例子,若是咱們在公司上班,有時候咱們中午會帶午餐,而後須要用微波爐來加熱一下,那這裏就有個問題了,在咱們去加熱午餐的時候,若是前面有同事在排隊,固然了,咱們都是有素質的人,出於禮貌,咱們也須要排隊,等待前面的同事操做完,後面來的同事固然也須要排在咱們後面,那這種狀況下,咱們能夠稱之爲公平策略
若是在前面正在熱午餐的同事,他有關係比較好的同事也來熱午餐,這個時候是否能夠插隊,若是這位同事插隊成功了,那麼他就能夠繼續熱午餐了,那麼咱們又得在後面等了,這種狀況咱們能夠稱之爲非公平策略。這兩種策略的區別就在於,公平策略會讓等待時間長的線程優先執行,非公平策略則是等待時間長的線程不必定會執行,存在一個搶佔資源的問題。若是從源碼的角度來看,那麼咱們就來簡單的說一下非公平策略的執行方式。操作系統

  • 非公平策略執行方式

一個線程首先會試探性的獲取一次鎖,若是獲取到,則將當前鎖設置爲該線程獨佔,若是沒有設置成功,則再次試探性的獲取一次,若是仍是沒有成功,則將該線程加入到等待隊列,後面再次等待獲取,看到這裏,可能有一些小夥伴不太明白了,那麼非公平是體如今哪裏,若是排在你前面的覺得同事恰好熱好午餐,而後你在後面玩手機,前面的同事尚未跟你說,「喂,該你去熱午餐了。」,而後這會兒又來了一個其餘人插隊,那麼做爲高素質的你,確定不能和別人計較了,好了,那就等着吧,非公平策略就體如今這裏了。這裏實現的方式很複雜,能夠一點一點去看,其中有用到AQSCAS等這些比較底層的原理,值得一提的是,如今JVMsynchronized的優化已經至關不錯,其性能表型已經和ReentrantLock不相上下,若是從推薦的角度來講,仍是推薦使用synchronized,除非須要使用一些比較高級的特性。線程

非互斥同步

接下來咱們再來聊一聊什麼是非互斥同步,互斥同步就是指阻塞同步,就是阻塞線程讓其一直等待某個鎖可用的過程,那麼非互斥同步剛和和這個是對立的,這裏用到了一個CAS的原理,這個是操做系統的指令,即compare and swap,比較並交換,說的簡單一點,就是若是在內存中一個值爲V,而後咱們又一個預估值A,而後還有一個新值B,若是V和A相同,那麼就把V的值替換爲B,而後返回V,這裏要說一下,不管是否能替換成功,都會返回V的值,這個過程稱做CAS。這種就涉及到操做系統級別了,由於在以前的阻塞同步中,阻塞線程而後再將其恢復得到鎖的過程,是比較耗費性能的,咱們知道,Java中的線程是對操做系統線程的一個映射,若是咱們在阻塞同步的時候,將一個線程掛起,而後再恢復,那麼這裏要經歷一個向核心態的轉換過程,消耗是比較巨大的。所以,在非互斥同步中,經過CAS這種方式,可以相對比較好的處理這個問題,可是有個問題須要明確一下,就是在Java中,這種處理方式,是經過這個類來完成的,sun.misc.Unsafe,這個類只能經過boot class loader來調用,要麼就是經過反射,或者經過某些方法來調用,好比AtomicInteger類中的incrementAndGet()方法,固然,還有不少這種方法,我的也是對CAS這種機制理解的比較片面,還須要更加深層次的研究。到這裏,咱們不得不說,Doug Lea的編碼能力實屬上乘,確實佩服!code

鎖優化

咱們再來談一談鎖優化,JVM爲咱們的代碼或者說其內部就作了一些優化方式,咱們就來簡單的說幾個。

  • 自旋

什麼是自旋,是的,有些小夥伴也能夠這麼理解,就是本身旋轉,固然這是開個玩笑,不過這種方式對於阻塞線程來講,是有必定的效果,簡單來講,就是若是一個線程得到了鎖,而後另外一個線程按照之前的方式只能阻塞等待,那麼JVM對這裏作了一個優化,就是讓這種等待的線程去執行一個循環,可是此時CPU的時間片是不會讓出去的,也就是說這裏的這個線程仍是佔有着一個處理時間,若是循環結束以後,這個鎖就能獲取了,那這個線程就拿到這個鎖繼續執行,這樣作的一個目的就在於咱們上面說的,線程從掛起到恢復須要像核心態的一個轉換,這個性能的消耗和佔用時間片的消耗比是很大的,可是同時也有一個問題就是若是自旋結束後,仍是沒有得到鎖,那麼這段時間性能的消耗就是浪費了,因此也很難權衡,所以在一些比較舊的JDK版本中,JVM是禁止使用自旋的。

  • 自適應自旋

這裏就是說若是一個線程在一次成功的自旋結束後,而且成功的得到了鎖而後成功運行,那麼JVM會「認爲」此次自旋是成功的,那麼下次若是繼續有線程發生自旋,那麼JVM可以判斷出來是否須要讓這個線程自旋來減小性能的消耗,從這裏來看,JVM仍是相對比較「機智」的。

  • 鎖消除

這個例子在上一篇文章中曾經提到過,對於字符串的拼接,咱們反編譯就能看出來,若是可以判斷不存在方法逃逸的狀況下,那麼JVM會對這種操做轉換爲StringBuilderappend操做,可能有的小夥伴會有疑問,爲何不轉換爲StringBuffer呢?由於JVM已經可以正確判斷出沒有方法逃逸,那麼若是再用線程安全類來處理也意義不大。

好了,這篇文章就先到這裏了,這裏只是很簡略的聊了一下在Java中的鎖的相關知識,更深刻的還有待後面繼續研究。

相關文章
相關標籤/搜索