基於ThreadLocal的無鎖併發發號器實現
阿新 • • 發佈:2018-12-12
ThreadLocal是一個執行緒級別的變數副本,它是對於執行緒隔離的,各個執行緒之間不能訪問非自己的ThreadLocal變數。
我們先來分析一下一個優秀的ID應該具備哪些特點?
- 全域性唯一性
- 有序性
- 能夠包含一些資訊(比如說時間資訊、生成機器資訊等)
為了保證ID的全域性唯一,在生成的時候我們應該對其做一些併發安全的處理,不然很可能就會出現重複ID,比如說ID的序列號是遞增的,那麼如何去保證在多執行緒訪問情況下生成的ID不重複呢?
我們最先想到的方式就是加鎖,每次只允許一個執行緒去操作這個累加的變數,這樣自然是能夠做到的,但是鎖競爭會帶來額外的效能開銷,那有沒有不加鎖的方式可以保證在多執行緒的情況下生成唯一ID呢?答案是肯定的,接下來我們看看如何使用ThreadLocal來實現無鎖化併發程式設計。
在發號器中最核心的程式碼就是ID序列號的生成,在本文中我也僅僅是對這一段進行分析(完整專案點這兒)
ThreadLocal是線上程內部存在的變數,因為執行緒之間的隔離,我們可以把我們能夠生成的ID去進行拆分,不同的執行緒去生成不同範圍內的ID,這樣就能夠保證ID不會重複生成了。
打個比方假如我們能夠生成100個ID,1~100,我們有兩個執行緒,第一個執行緒只生成1,3,5…這樣的ID,第二個執行緒只生成2,4,6…這樣的ID,從理論上來說,這樣的併發是不會重複的。
那麼我們的問題就轉化成了如何去分配生成的ID段,話不多說,直接上程式碼講解吧
public class Sender {
//把CPU核數作為執行緒數
private static final int THREADCOUNT=Runtime.getRuntime().availableProcessors();
//固定長度執行緒池
private static final ExecutorService POOL= Executors.newFixedThreadPool(THREADCOUNT);
//用執行緒ID對執行緒數取模作為執行緒ID
private static final ThreadLocal<Long> THREADID=new ThreadLocal<Long>( ){
@Override
protected Long initialValue() {
return Thread.currentThread().getId()%THREADCOUNT;
}
};
//用執行緒ID作為起始值
private static final ThreadLocal<Long> TARGET=new ThreadLocal<Long>(){
@Override
protected Long initialValue() {
return THREADID.get();
}
};
//用執行緒池中的執行緒去生成ID
private static Future<Long> doGet(){
return POOL.submit(()->{
Long t=TARGET.get();
TARGET.set(TARGET.get()+THREADCOUNT);
return t;
});
}
public static long get(){
Future<Long> future=doGet();
try {
long t=future.get();
return t;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return 0;
}
}
我們來測試一下是否會出現重複ID
public class Test {
//建立一個set去過濾生成的ID,如果發現ID少了肯定就發生了重複
public static ConcurrentSkipListSet set=new ConcurrentSkipListSet();
public static void main(String[] args) throws InterruptedException {
//用一個執行緒屏障去模擬併發,當有10個執行緒準備好之後就執行
CyclicBarrier barrier=new CyclicBarrier(10);
//生成10個執行緒
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
//把生成的ID放到set中去
for (int j = 0; j < 1000000; j++) {
set.add(Sender.get());
}
}).start();
}
//主執行緒睡眠20S等待程式跑完
Thread.sleep(20000);
//輸出ID個數,如果size==執行緒數*單個執行緒生成的ID數則認為是執行緒安全的
System.out.println(set.size());
}
}
結果如下 10000000
通過這種為執行緒劃分工作範圍的方式,我們可以利用ThreadLocal做到無鎖化的併發程式設計