自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,若是鎖已經被其它線程獲取,那麼該線程將循環等待,而後不斷的判斷鎖是否可以被成功獲取,直到獲取到鎖纔會退出循環。
獲取鎖的線程一直處於活躍狀態,可是並無執行任何有效的任務,使用這種鎖會形成busy-waiting。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
經過對輸出結果的分析咱們能夠得知,首先假定線程一在執行lock方法的時候得到了鎖,經過方法ide
cas.compareAndSet(null, current)函數
將引用改成線程一的引用,跳過while循環,執行打印函數性能
而線程二此時也進入lock方法,在執行比較操做的時候發現,expect value != update value,因而進入while循環,打印this
i am spinning。由如下紅字能夠得出結論,Java中的一個線程並非老是佔着cpu時間片不放,一直執行完的,而是採用搶佔式調度,因此出現了上面兩個線程交替執行的現象atom
Java線程的實現是經過映射到系統的輕量級線程上,輕量級線程有對應系統的內核線程,內核線程的調度由系統調度器來調度的,因此Java的線程調度方式取決於系統內核調度器,只不過恰好目前主流操做系統的線程實現都是搶佔式的。操作系統
使用自旋鎖會有如下一個問題:
1. 若是某個線程持有鎖的時間過長,就會致使其它等待獲取鎖的線程進入循環等待,消耗CPU。使用不當會形成CPU使用率極高。
2. 上面Java實現的自旋鎖不是公平的,即沒法知足等待時間最長的線程優先獲取鎖。不公平的鎖就會存在「線程飢餓」問題。
文章開始的時候的那段代碼,仔細分析一下就能夠看出,它是不支持重入的,即當一個線程第一次已經獲取到了該鎖,在鎖釋放以前又一次從新獲取該鎖,第二次就不能成功獲取到。因爲不知足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(); } }
本文到這裏就結束了,感謝看到最後的朋友,都看到最後了,點個贊再走啊,若有不對之處還請多多指正。