基于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做到无锁化的并发编程

你可能感兴趣的:(并发编程)