輕鬆搞懂Java中的自旋鎖

深夜裡的程式猿發表於2019-05-06

前言

在之前的文章《一文徹底搞懂面試中常問的各種“鎖”》中介紹了Java中的各種“鎖”,可能對於不是很瞭解這些概念的同學來說會覺得有點繞,所以我決定拆分出來,逐步詳細的介紹一下這些鎖的來龍去脈,那麼這篇文章就先來會一會“自旋鎖”。

正文

出現原因

在我們的程式中,如果存在著大量的互斥同步程式碼,當出現高併發的時候,系統核心態就需要不斷的去掛起執行緒和恢復執行緒,頻繁的此類操作會對我們系統的併發效能有一定影響。同時聰明的JVM開發團隊也發現,在程式的執行過程中鎖定“共享資源“的時間片是極短的,如果僅僅是為了這點時間而去不斷掛起、恢復執行緒的話,消耗的時間可能會更長,那就“撿了芝麻丟了西瓜”了。

而在一個多核的機器中,多個執行緒是可以並行執行的。如果當後面請求鎖的執行緒沒拿到鎖的時候,不掛起執行緒,而是繼續佔用處理器的執行時間,讓當前執行緒執行一個忙迴圈(自旋操作),也就是不斷在盯著持有鎖的執行緒是否已經釋放鎖,那麼這就是傳說中的自旋鎖了。

自旋鎖開啟

雖然在JDK1.4.2的時候就引入了自旋鎖,但是需要使用“-XX:+UseSpinning”引數來開啟。在到了JDK1.6以後,就已經是預設開啟了。下面我們自己來實現一個基於CAS的簡易版自旋鎖。


public class SimpleSpinningLock {

    /**
     * 持有鎖的執行緒,null表示鎖未被執行緒持有
     */
    private AtomicReference<Thread> ref = new AtomicReference<>();

    public void lock(){
        Thread currentThread = Thread.currentThread();
        while(!ref.compareAndSet(null, currentThread)){
            //當ref為null的時候compareAndSet返回true,反之為false
            //通過迴圈不斷的自旋判斷鎖是否被其他執行緒持有
        }
    }

    public void unLock() {
        Thread cur = Thread.currentThread();
        if(ref.get() != cur){
            //exception ...
        }
        ref.set(null);
    }
}

複製程式碼

簡簡單單幾行程式碼就實現了一個簡陋的自旋鎖,下面我們來測試一下

public class TestLock {

    static int count  = 0;

    public static void main(String[] args) throws InterruptedException {
       ExecutorService executorService = Executors.newFixedThreadPool(100);
       CountDownLatch countDownLatch = new CountDownLatch(100);
       SimpleSpinningLock simpleSpinningLock = new SimpleSpinningLock();
       for (int i = 0 ; i < 100 ; i++){
           executorService.execute(new Runnable() {
               @Override
               public void run() {
                   simpleSpinningLock.lock();
                   ++count;
                   simpleSpinningLock.unLock();
                   countDownLatch.countDown();
               }
           });

       }
       countDownLatch.await();
       System.out.println(count);
    }
}

// 多次執行輸出均為:100 ,實現了鎖的基本功能
複製程式碼

通過上面的程式碼可以看出,自旋就是在迴圈判斷條件是否滿足,那麼會有什麼問題嗎?如果鎖被佔用很長時間的話,自旋的執行緒等待的時間也會變長,白白浪費掉處理器資源。因此在JDK中,自旋操作預設10次,我們可以通過引數“-XX:PreBlockSpin”來設定,當超過來此引數的值,則會使用傳統的執行緒掛起方式來等待鎖釋放。

自適應自旋鎖

隨著JDK的更新,在1.6的時候,又出現了一個叫做“自適應自旋鎖”的玩意。它的出現使得自旋操作變得聰明起來,不再跟之前一樣死板。所謂的“自適應”意味著對於同一個鎖物件,執行緒的自旋時間是根據上一個持有該鎖的執行緒的自旋時間以及狀態來確定的。例如對於A鎖物件來說,如果一個執行緒剛剛通過自旋獲得到了鎖,並且該執行緒也在執行中,那麼JVM會認為此次自旋操作也是有很大的機會可以拿到鎖,因此它會讓自旋的時間相對延長。但是如果對於B鎖物件自旋操作很少成功的話,JVM甚至可能直接忽略自旋操作。因此,自適應自旋鎖是一個更加智慧,對我們的業務效能更加友好的一個鎖。

結語

本來想著在一篇文章裡面把“自旋鎖”,“鎖消除”,“鎖粗化”等一些鎖優化的概念都介紹完成的,但是發現可能篇幅會比較大,對於沒怎麼接觸過這一塊的同學來說理解起來會比較吃力,所以決定分開多個章節介紹,希望大家都不懂的地方可以多看幾遍,慢慢體會,相信你會有所收穫的。


公眾號博文同步Github倉庫,有興趣的朋友可以幫忙給個Star哦,碼字不易,感謝支援。

github.com/PeppaLittle…

推薦閱讀

如何優化程式碼中大量的if/else,switch/case?
如何提高使用Java反射的效率?
Java日誌正確使用姿勢
大白話搞懂什麼是同步/非同步/阻塞/非阻塞

有收穫的話,就點個贊吧

關注「深夜裡的程式猿」,分享最乾的乾貨

輕鬆搞懂Java中的自旋鎖

相關文章