zoukankan      html  css  js  c++  java
  • 什么是ThreadLocal?如何正确使用ThreadLocal?

    ThreadLocal线程本地存储

    多个线程同时读写同一个共享变量会造成并发问题,一种解决方案就是避免变量共享。我们可以使用线程封闭技术,即使用局部变量,每个线程都有各自的调用栈,局部变量就存在栈帧中,不会与其他线程共享。我们还可以使用线程本地存储ThreadLocal

    如何使用 ThreadLocal

    下面这段代码会为每个线程分配一个唯一的线程Id,同一个线程每次调用 get() 获得的 Id 是一样的,不同的线程调用 get() 获得的 Id 是不一样的。

    static class ThreadId {
      static final AtomicLong nextId = new AtomicLong(0);
      //定义ThreadLocal变量
      static final ThreadLocal<Long> tl=
        ThreadLocal.withInitial(()->nextId.getAndIncrement());
      //此方法会为每个线程分配一个唯一的Id
      static long get(){
        return tl.get();
      }
    }
    

    ThreadLocal

    ThreadLocal 中除了构造方法还有 4 个公共的方法:

    1. get():返回此线程局部变量当前副本中的值

    2. remove():移除此线程局部变量当前副本中的值

    3. set(T value):将线程局部变量当前副本中的值设置为指定值

    4. withInitial(Supplier<? extends S> supplier):返回此线程局部变量当前副本中的初始值

    ThreadLocal 的工作原理

    ThreadLocal 要实现的目标是:不同的线程对应不同的变量,很自然地可以想到创建一个 Map,其中 Key 是线程,Value 是线程对应的值。那么可以让 ThreadLocal 持有一个这样的 map,并提供对应的方法,就像下面这样:

    class MyThreadLocal<T> {
      Map<Thread, T> locals = 
        new ConcurrentHashMap<>();
      //获取线程变量  
      T get() {
        return locals.get(
          Thread.currentThread());
      }
      //设置线程变量
      void set(T t) {
        locals.put(
          Thread.currentThread(), t);
      }
    }
    

    这样设计会产生内存泄露的问题。ThreadLocal 持有 Map 的引用,Map 持有 Thread 对象的引用。这就意味着,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就不会被释放。ThreadLocal 对象的生命周期往往比线程要长得多,当长生命周期的对象持有短生命周期对象的引用,就会造成内存泄露问题

    在 Java 的设计中,ThreadLocal 持有的 Map 被命名为 ThreadLocalMap。ThreadLocalMap 并不是由 ThreadLocal 持有,而是由 Thread 持有。ThreadLocal 作为一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面。如下面的代码所示:

    public
    class Thread implements Runnable {
      // Thread 内部持有 ThreadLocalMap
      ThreadLocal.ThreadLocalMap threadLocals = null;
    }
    
    public class ThreadLocal<T> {
      
    	public T get() {
        // 1. 获取线程持有的 ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        
        // 2. 在Map中查找变量
        if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
          }
        }
        return setInitialValue();
      }
      
      static class ThreadLocalMap{
    		
        // Entry定义,是一个弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
          /** The value associated with this ThreadLocal. */
          Object value;
          Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
          }
        }
        
        // 内部是数组而不是Map
        private Entry[] table;
        // 根据ThreadLocal查找Entry
        Entry getEntry(ThreadLocal key){
          //省略查找逻辑
          ...
        }
      }
    }
    

    ThreadLocal各引用之间的关系

    理解 ThreadLocal 的原理,要结合上面这张图和代码:

    1. 当前线程线程持有 ThreadLocalMap,ThreadLocalMap 持有 Entry;
    2. Entry 的 Key 是一个 ThreadLocal 实例,并且是一个弱引用,value 是我们要存储的值;

    Java 的实现中 Thread 持有 ThreadLocalMap,ThreadLocalMap 中的 Entry 对 ThreadLocal 的引用是弱引用,所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。

    ThreadLocal 与内存泄露

    在线程池中使用 ThreadLocal 任然可能会出现内存泄露。

    因为线程池中线程的存活时间太长了,往往是和应用程序同生共死的。这就意味着 Thread 持有的 ThreadLocalMap 一直不会被回收,ThreadLocalMap 中 Entry 对 ThreadLocal 的引用是弱引用,所以只要 ThreadLocal 的生命周期结束是可以被回收的。但是 Entry 对 Value 是强引用,即使 value 的生命周期结束也无法被回收,这就造成了内存泄露

    在线程池中如何正确使用 ThreadLocal?

    那在线程池中,我们该如何正确使用 ThreadLocal 呢?既然 JVM 无法帮我们释放对 value 的引用,那么我们就使用 try{}finally{} 手动释放资源:

    ExecutorService es;
    ThreadLocal tl;
    es.execute(()->{
      //ThreadLocal增加变量
      tl.set(obj);
      try {
        // 省略业务逻辑代码
      }finally {
        //手动清理ThreadLocal 
        tl.remove();
      }
    });
    

    相关文章

    面试再问ThreadLocal,别说你不会

    并发容器之ThreadLocal

    30 | 线程本地存储模式:没有共享,就没有伤害

  • 相关阅读:
    Docker——JVM 感知容器的 CPU 和 Memory 资源限制
    Redis——封装通用的Redis组件
    Redis——Springboot集成Redis集群
    Redis——Spring集成Redis集群
    SQL SERVER 聚集索引 非聚集索引 区别
    一个页面同时发起多个ajax请求,会出现阻塞情况
    firefox快速刷新error及解决办法
    js 右击事件
    SQL group by 分组后,同一组的排序后取第一条
    SqlServer触发器
  • 原文地址:https://www.cnblogs.com/shuiyj/p/13185081.html
Copyright © 2011-2022 走看看