聊聊高并發(fā)(七)實現(xiàn)幾種自旋鎖(二)

聊聊高并發(fā)(六)實現(xiàn)幾種自旋鎖(一) 這篇中實現(xiàn)了兩種基本的自旋鎖:TASLock和TTASLock,它們的問題是會進行頻繁的CAS操作阳液,引發(fā)大量的緩存一致性流量誓酒,導(dǎo)致鎖的性能不好诗轻。

對TTASLock的一種改進是BackoffLock,它會在鎖高爭用的情況下對線程進行回退屏歹,減少競爭隐砸,減少緩存一致性流量。但是BackoffLock有三個主要的問題:

1. 還是有大量的緩存一致性流量蝙眶,因為所有線程在同一個共享變量上旋轉(zhuǎn)季希,每一次成功的獲取鎖都會產(chǎn)生緩存一致性流量

2. 因為回退的存在,不能及時獲取鎖釋放的信息幽纷,存在一個時間差式塌,導(dǎo)致獲取鎖的時間變長

3. 不能保證無饑餓,有的線程可能一直無法獲取鎖

這篇會實現(xiàn)2種基于隊列的鎖友浸,來解決上面提到的三個問題峰尝。主要的思路是將線程組織成一個隊列,有4個優(yōu)點:

1. 每個線程只需要檢查它的前驅(qū)線程的狀態(tài)收恢,將自旋的變量從一個分散到多個武学,減少緩存一致性流量

2. 可以即使獲取鎖釋放的通知

3. 隊列提供了先來先服務(wù)的公平性

4. 無饑餓,隊列中的每個線程都能保證被執(zhí)行到

隊列鎖分為兩類伦意,一類是基于有界隊列火窒,一類是基于無界隊列。

先看一下基于有界隊列的隊列鎖默赂。 ArrayLock有3個特點:

1. 基于一個volatile數(shù)組來組織線程

2. 通過一個原子變量tail來表示對尾線程

3. 通過一個ThreadLocal變量給每個線程一個索引號暇赤,表示它位于隊列的哪個位置田藐。

package com.test.lock;
 
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * 有界隊列鎖,使用一個volatile數(shù)組來組織線程
 * 缺點是得預(yù)先知道線程的規(guī)模n蛔琅,所有線程獲取同一個鎖的次數(shù)不能超過n
 * 假設(shè)L把鎖疾捍,那么鎖的空間復(fù)雜度為O(Ln)
 * **/
public class ArrayLock implements Lock{
    // 使用volatile數(shù)組來存放鎖標(biāo)志奈辰, flags[i] = true表示可以獲得鎖
    private volatile boolean[] flags;
    
    // 指向新加入的節(jié)點的后一個位置
    private AtomicInteger tail;
    
    // 總?cè)萘?    private final int capacity;
    
    private ThreadLocal<Integer> mySlotIndex = new ThreadLocal<Integer>(){
         protected Integer initialValue() {
                return 0;
         }
    };
    
    public ArrayLock(int capacity){
        this.capacity = capacity;
        flags = new boolean[capacity];
        tail = new AtomicInteger(0);
        // 默認(rèn)第一個位置可獲得鎖
        flags[0] = true;
    }
    
    @Override
    public void lock() {
        int slot = tail.getAndIncrement() % capacity;
        mySlotIndex.set(slot);
        // flags[slot] == true 表示獲得了鎖, volatile變量保證鎖釋放及時通知
         while(!flags[slot]){
            
         }
     }
 
     @Override
     public void unlock() {
           int slot = mySlotIndex.get();
           flags[slot] = false;
           flags[(slot + 1) % capacity] = true;
    }
<pre name="code" class="java">
        public String toString(){
           return "ArrayLock";
        }

}

我們可以看到有界隊列鎖的缺點是:

1. 它必須知道線程的規(guī)模數(shù)乱豆,對于同一把鎖如果線程獲取的次數(shù)超過了n會出現(xiàn)線程狀態(tài)被覆蓋的問題

2. 空間復(fù)雜度是O(Ln)

3. 對于共享的volatile數(shù)組來保存線程獲取鎖的狀態(tài)奖恰,仍然可能存在緩存一致性。我們知道CPU讀取一次內(nèi)存時宛裕,會讀滿數(shù)據(jù)總線的位長瑟啃,比如64位總線,一次讀取64位長度的數(shù)據(jù)揩尸。那么對于boolean類型的數(shù)組蛹屿,boolean長度是1個字節(jié),那么一次讀取能讀到8個boolean變量岩榆,而高速緩存的一個緩存塊的長度也是64位错负,也就是說一個緩存塊上可以保存8個boolean變量坟瓢,所以如果一次CAS操作修改了一個變量導(dǎo)致一個緩存塊無效,它實際上可能導(dǎo)致8個變量失效犹撒。

解決辦法是把變量以8個長度為單位分散折联,比如flag[0] = thread1 flag[8] = thread2。這樣的問題是消耗的空間更大识颊。

無界隊列鎖可以克服有界隊列鎖的幾個問題诚镰。

1. 它使用鏈表來代替數(shù)組,實現(xiàn)無界隊列

2. 使用兩個ThreadLocal變量表示指針谊囚,一個指向自己的節(jié)點怕享,一個指向前一個節(jié)點

3. 使用一個原子引用變量指向隊尾

4. 空間復(fù)雜度降低,如果有L把鎖镰踏,n個線程函筋,每個線程只獲取一把鎖,那么空間復(fù)雜度為O(L + n)

5. 對同一個鎖奠伪,一個線程可以多次獲取而不增加空間復(fù)雜度

6. 當(dāng)線程結(jié)束后谨敛,GC會自動回收內(nèi)存

package com.test.lock;
 
import java.util.concurrent.atomic.AtomicReference;
 
/**
 * 無界隊列鎖,使用一個鏈表來組織線程
 * 假設(shè)L把鎖炊甲,n個線程,那么鎖的空間復(fù)雜度為O(L+n)
 * **/
public class CLHLock implements Lock{
    // 原子變量指向隊尾
    private AtomicReference<QNode> tail;
    // 兩個指針,一個指向自己的Node,一個指向前一個Node
    ThreadLocal<QNode> myNode;
    ThreadLocal<QNode> myPreNode;
    
    public CLHLock(){
        tail = new AtomicReference<QNode>(new QNode());
        myNode = new ThreadLocal<QNode>(){
            protected QNode initialValue(){
                return new QNode();
            }
        };
        myPreNode = new ThreadLocal<QNode>(){
            protected QNode initialValue(){
                return null;
            }
        };
    }
    
    @Override
    public void lock() {
        QNode node = myNode.get();
        node.lock = true;
        // CAS原子操作,保證原子性
        QNode preNode = tail.getAndSet(node);
        myPreNode.set(preNode);
        // volatile變量野崇,能保證鎖釋放及時通知
        // 只對前一個節(jié)點的狀態(tài)自旋,減少緩存一致性流量
        while(preNode.lock){
            
        }
    }
 
    @Override
    public void unlock() {
        QNode node = myNode.get();
        node.lock = false;
        // 把myNode指向preNode蕴侣,目的是保證同一個線程下次還能使用這個鎖蝠筑,因為myNode原來指向的節(jié)點有它的后一個節(jié)點的preNode引用
        // 防止這個線程下次lock時myNode.get獲得原來的節(jié)點
        myNode.set(myPreNode.get());
    }
    
    public static class QNode {
        volatile boolean lock;
    }
 
        public String toString(){
           return "CLHLock";
        }
 }

下面我們從正確性和平均獲取鎖的時間上來測試這兩種鎖挽封。

我們設(shè)計一個測試用例來驗證正確性: 使用50個線程對一個volatile變量++操作,由于volatile變量++操作不是原子的,在不加鎖的情況下,可能同時有多個線程同時對voaltile變量++, 最終的結(jié)果是無法預(yù)測的苗踪。然后使用這兩種鎖瓦呼,先獲取鎖再volatile變量++央串,由于volatile變量會防止重排序质和,并能保證可見性厦酬,我們可以確定如果鎖是正確獲取的昌讲,也就是說同一時刻只有一個線程對volatile變量++,那么結(jié)果肯定是順序的1到50筹裕。

先看不加鎖的情況

package com.test.lock;
 
public class Main {
    //private static Lock lock = new ArrayLock(150);
    
    private static Lock lock = new CLHLock();
    
    //private static TimeCost timeCost = new TimeCost(new TTASLock());
    
    private static volatile int value = 0;
    public static void method(){
        //lock.lock();
        System.out.println("Value: " + ++value);
        //lock.unlock();
    }
    
    public static void main(String[] args) {
        for(int i = 0; i < 50; i ++){
           Thread t = new Thread(new Runnable(){
    
           @Override
                  public void run() {
             method();
           }
                
               });
          t.start();
            }
    }
 
}

運行結(jié)果: 我們可以看到確實是發(fā)生的線程同時對volatile變量++的操作扎运,結(jié)果是無法預(yù)料的

Value: 1Value: 1Value: 2Value: 3Value: 4Value: 5Value: 6Value: 7Value: 8Value: 9Value: 10Value: 11Value: 13Value: 12Value: 14Value: 15Value: 16Value: 17Value: 18Value: 19Value: 20Value: 21Value: 22Value: 23Value: 24Value: 25Value: 26Value: 27Value: 28Value: 29Value: 30Value: 31Value: 32Value: 33Value: 34Value: 35Value: 36Value: 37Value: 38Value: 37Value: 39Value: 40Value: 41Value: 42Value: 43Value: 44Value: 45Value: 46Value: 47Value: 48Value: 50

使用有界隊列鎖:

package com.test.lock;
 
public class Main {
    private static Lock lock = new ArrayLock(100);
    
    //private static Lock lock = new CLHLock();
    
    //private static TimeCost timeCost = new TimeCost(new TTASLock());
    
    private static volatile int value = 0;
    public static void method(){
        lock.lock();
        System.out.println("Value: " + ++value);
        lock.unlock();
    }
    
    public static void main(String[] args) {
        for(int i = 0; i < 50; i ++){
            Thread t = new Thread(new Runnable(){
    
                @Override
                public void run() {
                    method();
                }
                
            });
            t.start();
        }
    }
 
}

運行結(jié)果是1到50的順序自增瑟曲,說明鎖保證了同一時刻只有一個線程在對volatile變量++,是正確的

Value: 1Value: 2Value: 3Value: 4Value: 5Value: 6Value: 7Value: 8Value: 9Value: 10Value: 11Value: 12Value: 13Value: 14Value: 15Value: 16Value: 17Value: 18Value: 19Value: 20Value: 21Value: 22Value: 23Value: 24Value: 25Value: 26Value: 27Value: 28Value: 29Value: 30Value: 31Value: 32Value: 33Value: 34Value: 35Value: 36Value: 37Value: 38Value: 39Value: 40Value: 41Value: 42Value: 43Value: 44Value: 45Value: 46Value: 47Value: 48Value: 49Value: 50

使用無界隊列鎖的情況也是正確的豪治,由于篇幅原因這里就不帖代碼了洞拨。

再看平均獲取鎖的時間。

package com.test.lock;
 
public class Main {
    private static Lock lock = new TimeCost(new CLHLock());
    
    //private static Lock lock = new CLHLock();
    
    //private static TimeCost timeCost = new TimeCost(new TTASLock());
    
    private static volatile int value = 0;
    public static void method(){
        lock.lock();
        //System.out.println("Value: " + ++value);
        lock.unlock();
    }
    
    public static void main(String[] args) {
        for(int i = 0; i < 100; i ++){
            Thread t = new Thread(new Runnable(){
    
                @Override
                public void run() {
                    method();
                }
                
            });
            t.start();
        }
    }
 
}

在100個線程并發(fā)的情況下负拟,

ArrayLock獲取鎖的平均時間是: 719550 ns

CLHLock獲取鎖的平均時間是: 488577 ns

可以看到烦衣,隊列鎖在使用多個共享變量自旋的情況下,減少了一致性流量掩浙,比TASLock和TTASLock 提高了程序的性能花吟。而CLHLock比ArrayLock有更好的擴展性和性能,是一種很好的自旋鎖實現(xiàn)厨姚。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衅澈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谬墙,更是在濱河造成了極大的恐慌今布,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拭抬,死亡現(xiàn)場離奇詭異部默,居然都是意外死亡,警方通過查閱死者的電腦和手機造虎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門傅蹂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事份蝴±绻Γ” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵婚夫,是天一觀的道長波桩。 經(jīng)常有香客問我,道長请敦,這世上最難降的妖魔是什么镐躲? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮侍筛,結(jié)果婚禮上萤皂,老公的妹妹穿的比我還像新娘。我一直安慰自己匣椰,他們只是感情好裆熙,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著禽笑,像睡著了一般入录。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上佳镜,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天僚稿,我揣著相機與錄音,去河邊找鬼蟀伸。 笑死蚀同,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的啊掏。 我是一名探鬼主播蠢络,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼迟蜜!你這毒婦竟也來了刹孔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤娜睛,失蹤者是張志新(化名)和其女友劉穎髓霞,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體微姊,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡酸茴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年分预,在試婚紗的時候發(fā)現(xiàn)自己被綠了兢交。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡笼痹,死狀恐怖配喳,靈堂內(nèi)的尸體忽然破棺而出酪穿,到底是詐尸還是另有隱情,我是刑警寧澤晴裹,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布被济,位于F島的核電站,受9級特大地震影響涧团,放射性物質(zhì)發(fā)生泄漏只磷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一泌绣、第九天 我趴在偏房一處隱蔽的房頂上張望钮追。 院中可真熱鬧,春花似錦阿迈、人聲如沸元媚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刊棕。三九已至,卻和暖如春待逞,著一層夾襖步出監(jiān)牢的瞬間甥角,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工识樱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蜈膨,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓牺荠,卻偏偏與公主長得像翁巍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子休雌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 本文是我自己在秋招復(fù)習(xí)時的讀書筆記灶壶,整理的知識點,也是為了防止忘記杈曲,尊重勞動成果驰凛,轉(zhuǎn)載注明出處哦!如果你也喜歡担扑,那...
    波波波先森閱讀 11,273評論 4 56
  • 一個簡單的單例示例 單例模式可能是大家經(jīng)常接觸和使用的一個設(shè)計模式恰响,你可能會這么寫 publicclassUnsa...
    Martin說閱讀 2,230評論 0 6
  • 昨晚,潔離開微社涌献,激起了大家內(nèi)心的波瀾胚宦。麗萍說,感覺自己心好痛。理解她的感覺枢劝。年初微社有幾個人離開井联,對麗萍來說,也...
    綻蕊向陽閱讀 182評論 4 0
  • 理想效果 實際效果 思考 1.位置上的變化(spring動畫) UIView CASpringAnimation ...
    上官soyo閱讀 1,386評論 1 8
  • 一周目 2017.2 感覺作者是有觀察力卻不太能分析您旁,沒有活明白的人烙常。陷入自己的三觀局限之中,從而寫下了自己的生命...
    弦安閱讀 455評論 0 0