1. 程式人生 > >基於ThreadLocal的無鎖併發發號器實現

基於ThreadLocal的無鎖併發發號器實現

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做到無鎖化的併發程式設計