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