摘要:在本例中,講述的無鎖來自于并發包我們將這個無鎖的稱為。在這里,我們使用二維數組來表示的內部存儲,如下變量存放所有的內部元素。為什么使用二維數組去實現一個一維的呢這是為了將來進行動態擴展時可以更加方便。
我們已經比較完整得介紹了有關無鎖的概念和使用方法。相對于有鎖的方法,使用無鎖的方式編程更加考驗一個程序員的耐心和智力。但是,無鎖帶來的好處也是顯而易見的,第一,在高并發的情況下,它比有鎖的程序擁有更好的性能;第二,它天生就是死鎖免疫的。就憑借這2個優勢,就值得我們冒險嘗試使用無鎖的并發。
這里,我想向大家介紹一種使用無鎖方式實現的Vector。通過這個案例,我們可以更加深刻地認識無鎖的算法,同時也可以學習一下有關Vector實現的細節和算法技巧。(在本例中,講述的無鎖Vector來自于amino并發包)
我們將這個無鎖的Vector稱為LockFreeVector。它的特點是可以根據需求動態擴展其內部空間。在這里,我們使用二維數組來表示LockFreeVector的內部存儲,如下:
private final AtomicReferenceArray> buckets;
變量buckets存放所有的內部元素。從定義上看,它是一個保存著數組的數組,也就是通常的二維數組。特別之處在于這些數組都是使用CAS的原子數組。為什么使用二維數組去實現一個一維的Vector呢?這是為了將來Vector進行動態擴展時可以更加方便。我們知道,AtomicReferenceArray內部使用Object[]來進行實際數據的存儲,這使得動態空間增加特別的麻煩,因此使用二維數組的好處就是為將來增加新的元素。
此外,為了更有序的讀寫數組,定義一個稱為Descriptor的元素。它的作用是使用CAS操作寫入新數據。
static class Descriptor{ public int size; volatile WriteDescriptor writeop; public Descriptor(int size, WriteDescriptor writeop) { this.size = size; this.writeop = writeop; } public void completeWrite() { WriteDescriptor tmpOp = writeop; if (tmpOp != null) { tmpOp.doIt(); writeop = null; // this is safe since all write to writeop use // null as r_value. } } } static class WriteDescriptor { public E oldV; public E newV; public AtomicReferenceArray addr; public int addr_ind; public WriteDescriptor(AtomicReferenceArray addr, int addr_ind, E oldV, E newV) { this.addr = addr; this.addr_ind = addr_ind; this.oldV = oldV; this.newV = newV; } public void doIt() { addr.compareAndSet(addr_ind, oldV, newV); } }
上述代碼第4行定義的Descriptor構造函數接收2個參數,第一個為整個Vector的長度,第2個為一個writer。最終,寫入數據是通過writer進行的(通過completeWrite()方法)。第24行,WriteDescriptor的構造函數接收4個參數。第一個參數addr表示要修改的原子數組,第二個參數為要寫入的數組索引位置,第三個oldV為期望值,第4個newV為需要寫入的值。
在構造LockFreeVector時,顯然需要將buckets和descriptor進行初始化。
public LockFreeVector() { buckets = new AtomicReferenceArray>(N_BUCKET); buckets.set(0, new AtomicReferenceArray (FIRST_BUCKET_SIZE)); descriptor = new AtomicReference >(new Descriptor (0, null)); }
在這里N_BUCKET為30,也就是說這個buckets里面可以存放一共30個數組(由于數組無法動態增長,因此數組總數也就不能超過30個)。并且將第一個數組的大小為FIRST_BUCKET_SIZE為8。到這里,大家可能會有一個疑問,如果每個數組8個元素,一共30個數組,那豈不是一共只能存放240個元素嗎?
如果大家了解JDK內的Vector實現,應該知道,Vector在進行空間增長時,默認情況下,每次都會將總容量翻倍。因此,這里也借鑒類似的思想,每次空間擴張,新的數組的大小為原來的2倍(即每次空間擴展都啟用一個新的數組),因此,第一個數組為8,第2個就是16,第3個就是32。以此類推,因此30個數組可以支持的總元素達到。
這數值已經超過了2^33,即在80億以上。因此,可以滿足一般的應用。
當有元素需要加入LockFreeVector時,使用一個名為push_back()的方法,將元素壓入Vector最后一個位置。這個操作顯然就是LockFreeVector的最為核心的方法,也是最能體現CAS使用特點的方法,它的實現如下:
public void push_back(E e) { Descriptordesc; Descriptor newd; do { desc = descriptor.get(); desc.completeWrite(); int pos = desc.size + FIRST_BUCKET_SIZE; int zeroNumPos = Integer.numberOfLeadingZeros(pos); int bucketInd = zeroNumFirst - zeroNumPos; if (buckets.get(bucketInd) == null) { int newLen = 2 * buckets.get(bucketInd - 1).length(); if (debug) System.out.println("New Length is:" + newLen); buckets.compareAndSet(bucketInd, null, new AtomicReferenceArray (newLen)); } int idx = (0x80000000>>>zeroNumPos) ^ pos; newd = new Descriptor (desc.size + 1, new WriteDescriptor ( buckets.get(bucketInd), idx, null, e)); } while (!descriptor.compareAndSet(desc, newd)); descriptor.get().completeWrite(); }
可以看到,這個方法主體部分是一個do-while循環,用來不斷嘗試對descriptor的設置。也就是通過CAS保證了descriptor的一致性和安全性。在第23行,使用descriptor將數據真正地寫入數組中。這個descriptor寫入的數據由20~21行構造的WriteDescriptor決定。
摘自:實戰Java高并發程序設計
【實戰Java高并發程序設計1】Java中的指針:Unsafe類
【實戰Java高并發程序設計2】無鎖的對象引用:AtomicReference
【實戰Java高并發程序設計 3】帶有時間戳的對象引用:AtomicStampedReference
【實戰Java高并發程序設計 4】數組也能無鎖AtomicIntegerArray
【實戰Java高并發程序設計5】讓普通變量也享受原子操作
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65760.html
摘要:實戰高并發程序設計連載中的指針類和非常類似,不同之處就在于是對整數的封裝,而則對應普通的對象引用。這樣,當前線程就無法正確判斷這個對象究竟是否被修改過。摘自實戰高并發程序設計一書 【實戰Java高并發程序設計】連載1–Java中的指針:Unsafe類 AtomicReference和AtomicInteger非常類似,不同之處就在于AtomicInteger是對整數的封裝,而Atomi...
摘要:當前可用的原子數組有和,分別表示整數數組型數組和普通的對象數組。摘自實戰高并發程序設計一書實戰高并發程序設計中的指針類實戰高并發程序設計無鎖的對象引用實戰高并發程序設計帶有時間戳的對象引用 除了提供基本數據類型外,JDK還為我們準備了數組等復合結構。當前可用的原子數組有:AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,分別...
摘要:有時候,由于初期考慮不周,或者后期的需求變化,一些普通變量可能也會有線程安全的需求。它可以讓你在不改動或者極少改動原有代碼的基礎上,讓普通的變量也享受操作帶來的線程安全性,這樣你可以修改極少的代碼,來獲得線程安全的保證。 有時候,由于初期考慮不周,或者后期的需求變化,一些普通變量可能也會有線程安全的需求。如果改動不大,我們可以簡單地修改程序中每一個使用或者讀取這個變量的地方。但顯然,這...
摘要:相比與其他操作系統包括其他類系統有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。因為多線程競爭鎖時會引起上下文切換。減少線程的使用。很多編程語言中都有協程。所以如何避免死鎖的產生,在我們使用并發編程時至關重要。 系列文章傳送門: Java多線程學習(一)Java多線程入門 Java多線程學習(二)synchronized關鍵字(1) java多線程學習(二)syn...
摘要:通過指令重排可以減少流水線停頓提升巨大效率原則程序順序原則一個線程內保證語義的串行性。鎖規則解鎖必然發生在隨后的加鎖前。線程的方法先于它的每一個動作。 1. 基本概念 同步(Synchronous)和異步(Asynchronous) 并發(Conncurrency)和并行(Parallelism) 臨界區 阻塞(Blocking)與非阻塞(Non-Blocking) 死鎖(Deadl...
閱讀 3939·2021-10-09 09:43
閱讀 2871·2021-10-08 10:05
閱讀 2734·2021-09-08 10:44
閱讀 882·2019-08-30 15:52
閱讀 2809·2019-08-26 17:01
閱讀 3016·2019-08-26 13:54
閱讀 1650·2019-08-26 10:48
閱讀 807·2019-08-23 14:41