Java併發編程——深刻理解自旋鎖

1.什麼是自旋鎖

自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,若是鎖已經被其它線程獲取,那麼該線程將循環等待,而後不斷的判斷鎖是否可以被成功獲取,直到獲取到鎖纔會退出循環。
獲取鎖的線程一直處於活躍狀態,可是並無執行任何有效的任務,使用這種鎖會形成busy-waitingjava

2.Java如何實現自旋鎖?

先看一個實現自旋鎖的例子,java.util.concurrent包裏提供了不少面向併發編程的類. 使用這些類在多核CPU的機器上會有比較好的性能.主要緣由是這些類裏面大多使用(失敗-重試方式的)樂觀鎖而不是synchronized方式的悲觀鎖.編程

class spinlock {
    private AtomicReference<Thread> cas;
    spinlock(AtomicReference<Thread> cas){
        this.cas = cas;
    }
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) { //爲何預期是null??
            // DO nothing
            System.out.println("I am spinning");
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

lock()方法利用的CAS,當第一個線程A獲取鎖的時候,可以成功獲取到,不會進入while循環,若是此時線程A沒有釋放鎖,另外一個線程B又來獲取鎖,此時因爲不知足CAS,因此就會進入while循環,不斷判斷是否知足CAS,直到A線程調用unlock方法釋放了該鎖。多線程

自旋鎖驗證代碼

package ddx.多線程;

import java.util.concurrent.atomic.AtomicReference;

public class 自旋鎖 {
    public static void main(String[] args) {
        AtomicReference<Thread> cas = new AtomicReference<Thread>();
        Thread thread1 = new Thread(new Task(cas));
        Thread thread2 = new Thread(new Task(cas));
        thread1.start();
        thread2.start();
    }

}

//自旋鎖驗證
class Task implements Runnable {
    private AtomicReference<Thread> cas;
    private spinlock slock ;

    public Task(AtomicReference<Thread> cas) {
        this.cas = cas;
        this.slock = new spinlock(cas);
    }

    @Override
    public void run() {
        slock.lock(); //上鎖
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }
}

經過以前的AtomicReference類建立了一個自旋鎖cas,而後建立兩個線程,分別執行,結果以下:併發

0
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
1
I am spin
I am spin
I am spin
I am spin
I am spin
2
3
4
5
6
7
8
9
I am spin
0
1
2
3
4
5
6
7
8
9

Java併發編程——深刻理解自旋鎖

經過對輸出結果的分析咱們能夠得知,首先假定線程一在執行lock方法的時候得到了鎖,經過方法ide

cas.compareAndSet(null, current)函數

將引用改成線程一的引用,跳過while循環,執行打印函數性能

而線程二此時也進入lock方法,在執行比較操做的時候發現,expect value != update value,因而進入while循環,打印this

i am spinning。由如下紅字能夠得出結論,Java中的一個線程並非老是佔着cpu時間片不放,一直執行完的,而是採用搶佔式調度,因此出現了上面兩個線程交替執行的現象atom

Java線程的實現是經過映射到系統的輕量級線程上,輕量級線程有對應系統的內核線程,內核線程的調度由系統調度器來調度的,因此Java的線程調度方式取決於系統內核調度器,只不過恰好目前主流操做系統的線程實現都是搶佔式的。操作系統

3.自旋鎖存在的問題

使用自旋鎖會有如下一個問題:
1. 若是某個線程持有鎖的時間過長,就會致使其它等待獲取鎖的線程進入循環等待,消耗CPU。使用不當會形成CPU使用率極高。
2. 上面Java實現的自旋鎖不是公平的,即沒法知足等待時間最長的線程優先獲取鎖。不公平的鎖就會存在「線程飢餓」問題。

4.自旋鎖的優勢

  1. 自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;不會使線程進入阻塞狀態,減小了沒必要要的上下文切換,執行速度快
  2. 非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候須要從內核態恢復,須要線程上下文切換。 (線程被阻塞後便進入內核(Linux)調度狀態,這個會致使系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能)

5.可重入的自旋鎖和不可重入的自旋鎖

文章開始的時候的那段代碼,仔細分析一下就能夠看出,它是不支持重入的,即當一個線程第一次已經獲取到了該鎖,在鎖釋放以前又一次從新獲取該鎖,第二次就不能成功獲取到。因爲不知足CAS,因此第二次獲取會進入while循環等待,而若是是可重入鎖,第二次也是應該可以成功獲取到的。
並且,即便第二次可以成功獲取,那麼當第一次釋放鎖的時候,第二次獲取到的鎖也會被釋放,而這是不合理的。

例如將代碼改爲以下:

@Override
    public void run() {
        slock.lock(); //上鎖
        slock.lock(); //再次獲取本身的鎖!因爲不可重入,則會陷入循環
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }

則運行結果將會無限打印,陷入無終止的循環! 

爲了實現可重入鎖,咱們須要引入一個計數器,用來記錄獲取鎖的線程數。

public class ReentrantSpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    private int count;
    public void lock() {
        Thread current = Thread.currentThread();
        if (current == cas.get()) { // 若是當前線程已經獲取到了鎖,線程數增長一,而後返回
            count++;
            return;
        }
        // 若是沒獲取到鎖,則經過CAS自旋
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread cur = Thread.currentThread();
        if (cur == cas.get()) {
            if (count > 0) {// 若是大於0,表示當前線程屢次獲取了該鎖,釋放鎖經過count減一來模擬
                count--;
            } else {// 若是count==0,能夠將鎖釋放,這樣就能保證獲取鎖的次數與釋放鎖的次數是一致的了。
                cas.compareAndSet(cur, null);
            }
        }
    }
}

一樣lock方法會先判斷是否當前線程已經拿到了鎖,拿到了就讓count加一,可重入,而後直接返回!而unlock方法則會首先判斷當前線程是否拿到了鎖,若是拿到了,就會先判斷計數器,不斷減一,不斷解鎖!

 可重入自旋鎖代碼驗證

//可重入自旋鎖驗證
class Task1 implements Runnable{
    private AtomicReference<Thread> cas;
    private ReentrantSpinLock slock ;

    public Task1(AtomicReference<Thread> cas) {
        this.cas = cas;
        this.slock = new ReentrantSpinLock(cas);
    }

    @Override
    public void run() {
        slock.lock(); //上鎖
        slock.lock(); //再次獲取本身的鎖!沒問題!
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock(); //釋放一層,但此時count爲1,不爲零,致使另外一個線程依然處於忙循環狀態,因此加鎖和解鎖必定要對應上,避免出現另外一個線程永遠拿不到鎖的狀況
        slock.unlock();
    }
}

6.自旋鎖與互斥鎖異同點

  • 自旋鎖與互斥鎖都是爲了實現保護資源共享的機制。
  • 不管是自旋鎖仍是互斥鎖,在任意時刻,都最多隻能有一個保持者。
  • 獲取互斥鎖的線程,若是鎖已經被佔用,則該線程將進入睡眠狀態;獲取自旋鎖的線程則不會睡眠,而是一直循環等待鎖釋放。

7.總結

  • 自旋鎖:線程獲取鎖的時候,若是鎖被其餘線程持有,則當前線程將循環等待,直到獲取到鎖。
  • 自旋鎖等待期間,線程的狀態不會改變,線程一直是用戶態而且是活動的(active)。
  • 自旋鎖若是持有鎖的時間太長,則會致使其它等待獲取鎖的線程耗盡CPU。
  • 自旋鎖自己沒法保證公平性,同時也沒法保證可重入性。
  • 基於自旋鎖,能夠實現具有公平性和可重入性質的鎖

結尾

本文到這裏就結束了,感謝看到最後的朋友,都看到最後了,點個贊再走啊,若有不對之處還請多多指正。

相關文章
相關標籤/搜索