1.什麼是自旋鎖
自旋鎖(spinlock):是指當一個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那麼該執行緒將迴圈等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出迴圈。
獲取鎖的執行緒一直處於活躍狀態,但是並沒有執行任何有效的任務,使用這種鎖會造成busy-waiting。
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
透過對輸出結果的分析我們可以得知,首先假定執行緒一在執行lock方法的時候獲得了鎖,透過方法
cas.compareAndSet(null, current)
將引用改為執行緒一的引用,跳過while迴圈,執行列印函式
而執行緒二此時也進入lock方法,在執行比較操作的時候發現,expect value != update value,於是進入while迴圈,列印
i am spinning。由以下紅字可以得出結論,Java中的一個執行緒並不是總是佔著cpu時間片不放,一直執行完的,而是採用搶佔式排程,所以出現了上面兩個執行緒交替執行的現象
Java執行緒的實現是透過對映到系統的輕量級執行緒上,輕量級執行緒有對應系統的核心執行緒,核心執行緒的排程由系統排程器來排程的,所以Java的執行緒排程方式取決於系統核心排程器,只不過剛好目前主流作業系統的執行緒實現都是搶佔式的。
3.自旋鎖存在的問題
使用自旋鎖會有以下一個問題:
1. 如果某個執行緒持有鎖的時間過長,就會導致其它等待獲取鎖的執行緒進入迴圈等待,消耗CPU。使用不當會造成CPU使用率極高。
2. 上面Java實現的自旋鎖不是公平的,即無法滿足等待時間最長的執行緒優先獲取鎖。不公平的鎖就會存在“執行緒飢餓”問題。
4.自旋鎖的優點
- 自旋鎖不會使執行緒狀態發生切換,一直處於使用者態,即執行緒一直都是active的;不會使執行緒進入阻塞狀態,減少了不必要的上下文切換,執行速度快
- 非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入核心態,當獲取到鎖的時候需要從核心態恢復,需要執行緒上下文切換。 (執行緒被阻塞後便進入核心(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();
}
}
- 自旋鎖與互斥鎖都是為了實現保護資源共享的機制。
- 無論是自旋鎖還是互斥鎖,在任意時刻,都最多隻能有一個保持者。
- 獲取互斥鎖的執行緒,如果鎖已經被佔用,則該執行緒將進入睡眠狀態;獲取自旋鎖的執行緒則不會睡眠,而是一直迴圈等待鎖釋放。
- 自旋鎖:執行緒獲取鎖的時候,如果鎖被其他執行緒持有,則當前執行緒將迴圈等待,直到獲取到鎖。
- 自旋鎖等待期間,執行緒的狀態不會改變,執行緒一直是使用者態並且是活動的(active)。
- 自旋鎖如果持有鎖的時間太長,則會導致其它等待獲取鎖的執行緒耗盡CPU。
- 自旋鎖本身無法保證公平性,同時也無法保證可重入性。
- 基於自旋鎖,可以實現具備公平性和可重入性質的鎖。
本作品採用《CC 協議》,轉載必須註明作者和本文連結