ThreadLocal本地存储保证并发安全

ThreadLocal本地存储保证并发安全

前言引入

多线程因为并发执行带来了性能上的优势,同时也因为多线程间的数据竞争导致线程安全问题,我之前有提过可以利用不变性类Immutability来解决线程安全问题,这个办法的本质是让线程不直接修改属性值来保证线程安全,其实还有一种办法那就是线程间不共享,各自读写各自线程的变量,没有共享便没有了伤害,这就是本地存储方案ThreadLocal的优势所在。

什么是ThreadLocal

ThreadLocal其实就是一种线程封闭的思想,本质有点像局部变量,所有的局部变量在方法入栈时保存在了栈帧的局部变量表内部,是当前线程所独有的不会共享给其它线程,这样就保证了线程安全,同样ThreadLocal也是如此主要作用是数据隔离,填充的数据只属于当前线程,变量的数据对其它线程而言是相对隔离的,保证了多线程场景不会被其它线程所篡改。

ThreadLocal 的使用方法

/**
 * 类的作用是给每一个线程分配一个id
 */
class ThreadId{
    static final AtomicLong nextId = new AtomicLong(0);

    // withInitial 创建线程局部变量
    static final ThreadLocal<Long> tl = ThreadLocal.withInitial(()->{
       return nextId.getAndIncrement();
    });

    //返回当前线程的局部变量的副本中的值。 如果变量没有当前线程的值,则首先将其初始化
    //为调用initialValue()方法返回的值
    static long get(){
        return tl.get();
    }
}

方法执行结果对比

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i <5 ; i++) {
        new Thread(()->{
            	System.out.println(Thread.currentThread().getName()+"===="+ThreadId.get());
        },"T"+i).start();
    }

    Thread.sleep(3000);

    System.out.println("=======================");
    for (int i = 0; i <5 ; i++) {
        System.out.println(Thread.currentThread().getName()+"===="+ThreadId.get());
    }
}

ThreadLocal本地存储保证并发安全_第1张图片

从上面可以看出,单线程多次调用tl.get会返回相同的nextId,而不同线程则需要重新调用ThreadLocal.withInitial方法进行一次nextId的赋值,多线程下nextId值没有重复说明线程间没有共享nextId变量。

ThreadLocal 的使用场景

ThreadLocal 做线程隔离在实际项目中使用并不多,不过可以简单例举几种常用但是被忽略的业务场景

Spring事务管理

spring为了保证单个线程中的数据库操作使用的是同一个数据库链接,通过事务传播可以管理多个事务配置之间切换等,只要知道数据库链接是采用ThreadLock管理即可。

public abstract class TransactionSynchronizationManager {

     //线程上下文中保存着【线程池对象:ConnectionHolder】的Map对象。线程可以通过该属性获取到同一个Connection对象。
    private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

    //事务同步器
    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
    
    // 事务名称  
    private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");
    // 事务是否是只读  
    private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");
    // 事务的隔离级别
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");
    // 事务是否开启   actual:真实的
    private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");
}

SimpleDataFormat正确使用

SimpleDataFormat之前出现过多线程下的线程安全问题,如下

public class ThreadLockDemo2 {
    public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws ParseException {
        for (int i = 0; i <10 ; i++) {
            int finalI = i;
            new Thread(()->{
                String format1 = simpleDateFormat.format(new Date(finalI *1000));
                System.out.println(Thread.currentThread().getName()+"=="+format1);
            },"T"+i).start();
        }
    }
}

预期结果应该如下所示

ThreadLocal本地存储保证并发安全_第2张图片

实际结果如下所示

ThreadLocal本地存储保证并发安全_第3张图片

原因分析如下,在SimpleDateFormat类的format实现类中有如下操作

public class SimpleDateFormat extends DateFormat {
   // DateFormat继承过来的
   protected Calendar calendar;
    
   private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        // 多个线程之间共享变量calendar,并修改calendar
        calendar.setTime(date);
		// 代码省略
    }
}

如何解决呢?方法其实很多如下

  • SimpleDateFormat共享影响不共享就好了,将SimpleDateFormat变为局部变量。
  • 加锁synchronized执行format方法。
  • 采用1.8提供的线程安全工具类DateTimeFormat。
  • 采用ThreadLock做数据隔离,代码如下。
public class ThreadLockDemo2 {
    public static void main(String[] args) throws ParseException {
        /**
         * 注意点,在创建ThreadLocal对象时 如果赋值只是如下这种方式赋值,那么只有当前线程
         * 调用threadLocal.get()能够获取到,其它线程一律是空!!!!
         * ThreadLocal threadLocal = new ThreadLocal();
         * threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         */
        ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(()->{
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        });
        for (int i = 0; i <10 ; i++) {
            int finalI = i;

            new Thread(()->{
                SimpleDateFormat simpleDateFormat = threadLocal.get();
                String format1 = simpleDateFormat.format(new Date(finalI *1000));
                System.out.println(Thread.currentThread().getName()+"=="+format1);
            },"T"+i).start();
        }
    }
}

上下文参数传递

在项目中如果存在一个线程需要横跨若干方法调用,需要传递的对象也就是上下文(context),如果采用责任链的形式那么非常麻烦需要给每一个方法增加context参数,而采用ThreadLock就可以轻松解决。

class Test{
    // before
    public void work(){
        getInfo(user);
        checkInfo(user);
        setSomeThing(user);
        log(user);
    }
    
    // now
    public void work(){
        try{
            threadLocal.set(user);
            // 方法内部采用 user = threadLocal.get()即可获取
            getInfo(user);
            checkInfo(user);
            setSomeThing(user);
            log(user);
        }finally {
            threadLocal.remove();
        }
    }
}

ThreadLocal 的工作原理

存储结构

了解到ThreadLocal的使用和应用场景后如果我们自己设计一个ThreadLocal应该怎么做呢?

存储容器应该是Map类型,key为线程,value为对象,构造图应该如下所示。

ThreadLocal本地存储保证并发安全_第4张图片

代码语义化如下

public class MyThreadLock<T> {
    // 容器
    private Map<Thread,T> map = new ConcurrentHashMap<>();
    
    public void set(T object){
        Thread thread = Thread.currentThread();
        map.put(thread,object);
    }
    
    public Object get(Thread thread){
        return map.get(thread);
    }
    
    public void remove(){
        map.clear();
    }
}

但是JAVA真是这样实现的吗?显然并不是的,虽然JAVA中也存在一个Map名为ThreadLocalMap,但持有者并不是ThreadLocal类,而是Thread类,Thread类有属性threadLocals,其类型就是ThreadLocalMap,ThreadLocalMap的key类型为ThreadLocal

ThreadLocal本地存储保证并发安全_第5张图片

结构图如下所示

ThreadLocal本地存储保证并发安全_第6张图片

源码说明

class Thread {
  // 内部持有ThreadLocalMap
  ThreadLocal.ThreadLocalMap threadLocals = null;
    
  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
class ThreadLocal<T>{
  // 获取线程Thread类的属性threadLocals 也就是 ThreadLocalMap
  ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
  }
  // 省略其它逻辑
  public T get() {
        Thread t = Thread.currentThread();
        // 获取根据当前线程ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // this为当前调用get的ThreadLocal对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 初始化ThreadLocalMap对象
        return setInitialValue();
  }
  static class ThreadLocalMap{
    // 内部是数组而不是Map
    Entry[] table;
    // 根据ThreadLocal查找Entry
    Entry getEntry(ThreadLocal key){
      //省略查找逻辑
    }
    //Entry定义
    static class Entry extends WeakReference<ThreadLocal>{
       Object value;
       Entry(ThreadLocal<?> k, Object v) {
           super(k);
           value = v;
       }
    }
  }
}

从源码中也能解释为什么ThreadLocal能线程安全了,因为ThreadLocal的值存储在当前线程Thread类的ThreadLocalMap类型的threadLocals属性中,每个线程拥有自己的ThreadLocalMap互不干扰。

同时这里需要解释下WeakReference弱引用,什么是WeakReference弱引用呢?

顾名思义指在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了==只具有弱引用==的对象,不管当前内存空间足够与否,都会回收它的内存。

内存泄露风险

由于ThreadLocalMap和Thread的同生共死的,所以ThreadLocalMap一直不会回收,而ThreadLocal又是弱引用,当外界没有强引用ThreadLock对象后Entry对象中的key就会回收,而Entry中的value却被Entry强引用这时就算value的生命周期已经结束其实也无法回收value对象,从而造成内存泄露。

ThreadLocal本地存储保证并发安全_第7张图片

既然出现了内存泄露的风险那么如何解决呢?

JVM无法做到自动释放value对象那我们可以手动释放value对象即可,一般释放资源都是采用try{}finally{}的模式,对于ThreadLocal同样适用,在使用完ThreadLocal手动清空当前线程的Entry对象即可

ThreadLocal threadLocal = new ThreadLocal();
try {
    threadLocal.set("test");
    Object o = threadLocal.get();
}finally {
    // 手动移除  原理很简单就是将当前对象在Entry[]数组中查找,做数组元素移除操作
    threadLocal.remove();
}

怎么共享ThreadLock数据

ThreadLock做到了线程封闭的思想,但如果我就想指定线程间共享ThreadLock中的数据这个怎么处理呢?采用InheritableThreadLocal实现,先上代码

public static void main(String[] args) {
    ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    try {
        threadLocal.set("牛逼");

        new Thread(()->{
            System.out.println("牛逼不牛逼:"+threadLocal.get());
            new Thread(()->{
                System.out.println("子线程牛逼波:"+threadLocal.get());
            }).start();
        }).start();
    }finally {
        threadLocal.remove();
    }
}

在主线程中创建ThreadLocal对象后,在主线程内部创建的所有线程(也就是子线程)都能获取到值。

源码分析

// ThreadLocal 类
public void set(T value) {
    Thread t = Thread.currentThread();
    // 调用InheritableThreadLocal类的getMap 获取的是
    // Thread类的属性  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // 重点在这里 在第一次没有获取到ThreadLocalMap 创建map 
        // 调用InheritableThreadLocal类的createMap
        createMap(t, value);
}

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
	// 重写createMap方法
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

public class Thread{
	
    ThreadLocal.ThreadLocalMap threadLocals = null;
	
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    // inheritThreadLocals=true
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
		// 省略无数代码
        // parent指创建子线程的线程 从示例中看就是主线程main
        Thread parent = currentThread();
        // parent.inheritableThreadLocals 在threadLocal.set("牛逼");时已经不为空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            // 子线程的属性
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
		// 省略无数代码
    }
}

需要注意的是生产下还是尽量不使用InheritableThreadLocal,不仅仅是有内存泄露的风险还有一个问题是线程池中的线程是动态创建的,容易造成继承关系混乱,如果业务逻辑依赖InheritableThreadLocal,可能造成业务逻辑计算错误,这个比内存泄露更加难以排查。

你可能感兴趣的:(并发编程专栏,java,开发语言)