多线程并发数据访问,确保数据安全至关重要,常用保证数据安全的方法有对代码synchronized锁、Lock锁,以及基于CAS的原子类,这些都是通过数据共享保障数据安全的,今天聊一聊另一种方案ThreadLocal线程副本,实现线程间的数据隔离,达到数据安全的目标。
一、ThreadLocal数据共享
ThreadLocal是线程变量,ThreadLocal中填充的线程变量是当前线程,该变量对其他线程而言是隔离的,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程只可以访问自己内部的副本变量。
翻看源码,ThreadLocal是当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
每个线程都维护了自己的一个 TreadLocalMap
1、ThreadLocal数据隔离的案例
public static class Data{private final ThreadLocal<String> threadLocalData = ThreadLocal.withInitial(() -> "");public void warpInfo(String prefix){// 给当前线程变量设置值threadLocalData.set(prefix+"-"+threadLocalData.get());}public String getInfo(){/// 获取当前线程变量return threadLocalData.get();}public void clearInfo(){/// 变量使用完后一定要释放,原因有二:// 1、ThreadLocalMap的key是WeakReference<ThreadLocal<?>> 弱引用,当发生gc时候,key被回收,而value是强引用为无法被回收,造成整个threadLocalMap变量无法回收,最终引起内存泄露;// 2、在使用线程池的时候,线程复用,或造成线程变量覆盖,影响正常业务threadLocalData.remove();}}
private final static ExecutorService POOL = Executors.newFixedThreadPool(3);public static void main(String[] args) {Data data = new Data();for(int i=0;i< 8;i++){POOL.execute(() -> {try {data.warpInfo("subThread");System.out.println(Thread.currentThread().getName()+" data=> "+data.getInfo());} finally {data.clearInfo();}});}
执行结果:
2、ThreadLocal父子线程数据传递问题
先演示现象
Data data = new Data();data.warpInfo("mainThread");POOL.execute(()->{System.out.println("111->info= "+ data.getInfo());data.warpInfo("subThread");System.out.println("222-info= "+data.getInfo());});
不出意外,第一个输出是mainThread-,第二个输出是mainThread-subThread-,但是实际输出确出现意外了
为什么会出现这个现象,这是因为在子线程和主线程之间是无法传递数据的,这更加印证了ThreadLocal具有线程隔离的作用。但是有时候确实存在需求需要线程间传递数据,并且还要保证线程安全。为了解决这一问题,官方推出了InheritableThreadLocal专门应对此种情况。
二、InheritableThreadLocal主线程子线程间数据传递
InheritableThreadLocal是ThreadLocal的子类,通过Thread类维护inheritableThreadLocals变量,在createInheritedMap的时候获取了读线程的变量值
改造Data类,直接将ThreadLocal替换成InheritableThreadLocal
private final InheritableThreadLocal<String> threadLocalData = new InheritableThreadLocal<>();
执行结果,可以看到主线线程的变量值在子线程中可以正常取到。
三、TransmittableThreadLocal线程池线程之间数据传递
不乏存在线程池内部线程之间需要实现数据传递,针对此种需求,阿里的开源工具TransmittableThreadLocal可以有效解决此类问题。
先看下问题现象
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();threadLocal.set("main-thread");System.out.println(Thread.currentThread().getName()+" get local data start ===> "+threadLocal.get());ExecutorService pool = Executors.newFixedThreadPool(3);pool.execute(()-> System.out.println(Thread.currentThread().getName()+" get local data ===> "+threadLocal.get()));sleep(1);threadLocal.set("main-thread-new");System.out.println(Thread.currentThread().getName()+" get local data end ===> "+threadLocal.get());pool.execute(()-> System.out.println(Thread.currentThread().getName()+" get local data ===> "+threadLocal.get()));sleep(Integer.MAX_VALUE);
线程池里面线程执行第二次打印的结果如果是main-thread-new,那么InheritableThreadLocal
或许具备线程池中传递线程变量的能力,看下执行结果并不理想
线程池中第二次获取主线程修改后的变量是不可见的。解决这个问题只需要稍微改造,引入
TransmittableThreadLocal即可:
TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();threadLocal.set("main-thread");System.out.println(Thread.currentThread().getName()+" get local data start ===> "+threadLocal.get());ExecutorService pool = TtlExecutors.getTtlExecutorService( Executors.newFixedThreadPool(1));pool.execute(()-> System.out.println(Thread.currentThread().getName()+" get local data ===> "+threadLocal.get()));sleep(1);threadLocal.set("main-thread-new");System.out.println(Thread.currentThread().getName()+" get local data end ===> "+threadLocal.get());pool.execute(()-> System.out.println(Thread.currentThread().getName()+" get local data ===> "+threadLocal.get()));sleep(Integer.MAX_VALUE);
至此完美解决线程变量线程池线程传递。