ThreadLocal使用场景和阅读源码

ThreadLocal其实可以理解成每个线程独有的一块区域,线程间不能相互影响的一块区域(其中父子线程可以通过InheritableThreadLocal实现子线程读取父线程ThreadLocal的数据)。

于是乎,我搜索了下项目中使用到ThreadLocal的地方。

在这里插入图片描述

说说用到的几个场景:
  • 对象隔离(线程需要一个独享的对象,例如SimpleDateFormat

    • 单线程

      public class Test {
      	private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
          public static void main(String[] args) {
              simpleThread();
          }
      
          private static void simpleThread() {
              new Thread(){
                  @Override
                  public void run() {
                      while(true) {
                          //因为只有一个线程所以就类似是睡眠2000毫ms
                          this.join(2000);
                          System.out.println(this.getName() + ":" + sdf.parse("2013-05-24 06:02:20"));
                      }
                  }
              }.start();
          }
      }
      --------------------------------------------------------------
      Thread-0:Fri May 24 06:02:20 CST 2013
      Thread-0:Fri May 24 06:02:20 CST 2013
      Thread-0:Fri May 24 06:02:20 CST 2013
      Thread-0:Fri May 24 06:02:20 CST 2013
      

      这里没有问题,因为单线程运行,不会发生线程安全问题。这里要注意的是SimpleDateFormat是线程不安全的对象,当它在格式化时间的时候里边会操作内部的Calendar对象(没有线程安全处理)所以导致在多线程中会出错。

    • 多线程

      private static void manyThread() {
          for(int i = 0; i < 3; i++){
              new Thread(){
                  @Override
                  public void run() {
                      while(true) {
                  //这里因为是多线程是让其他线程执行,这里就是为了更好的复现SimpleDateFormat线程不安全操作
                          this.join(2000);
                          System.out.println(this.getName() + ":" + sdf.parse("2013-05-24 06:02:20"));
                      }
                  }
              }.start();
          }
      }
      --------------------------------------------------------------
      Thread-2:Tue Feb 20 18:58:40 CST 20132024
      Thread-1:Tue Feb 20 18:58:40 CST 20132024
      Thread-0:Fri May 24 06:02:20 CST 2013
      Thread-1:Mon May 24 06:02:20 CST 1
      

      调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

      这里有人会说那定义成局部变量就可以了呗。是的,不是为了减少new对象的开支嘛。哈哈哈哈哈

      这下ThreadLocal来了,我们只要保证每个线程有自己的SimpleDataFormat并且只用自己的SimpleDataFormat就可以保证不出错了。那这个场景和ThreadLocal特性一毛一样啊。

    • 多线程使用ThreadLocal解决线程安全

      public class Test {
      	
          //每个线程都会有一个自己的SimpleDateFormat
      	private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
              @Override
              protected DateFormat initialValue() {
                  return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
              }
          };
          public static Date parse(String strDate) throws ParseException {
              return threadLocal.get().parse(strDate);
          }
          public static void main(String[] args) {
              simpleThread();
          }
          private static void simpleThread() {
              for(int i = 0; i < 3; i++){
                  new Thread(){
                      @Override
                      public void run() {
                          while(true) {
                              this.join(2000);
                              //这里用的parse方法是从ThreadLocal取出来的SimpleDateFormat
                              System.out.println(this.getName() + ":" + Test.parse("2013-05-24 06:02:20"));
                          }
                      }
                  }.start();
              }
          }
      }
      --------------------------------------------------------------
      Thread-1:Fri May 24 06:02:20 CST 2013
      Thread-2:Fri May 24 06:02:20 CST 2013
      Thread-0:Fri May 24 06:02:20 CST 2013
      Thread-1:Fri May 24 06:02:20 CST 2013
      
  • 对象传递

    线程需要保存全局变量,可以让不同的方法直接使用,而不需要让数据作为参数层层传递。**强调的是同一个请求内(同一个线程内)不同方法间的共享。**当然Map也可以存储上述业务信息。多线程同时工作时,需要保证线程安全。例如,采用静态ConcurrentHashMap变量,将线程ID作为key,业务数据作为Value保存,可以做到线程间隔离。可以实现的方式有很多。

    我们这里简单写个存储用户信息的实现。

    public class RpcFilter implements Filter {
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,ServletException {
        //从request信息中读取session信息
        HttpSession session = ((HttpServletRequest) request).getSession(true);
        //取得缓存中的用户信息
        Object sessionUser = session.getAttribute(RpcHolder.DEFAULT_USER_INFO);
        if (sessionUser != null) {
          //如果存在则先清除
          if (RpcHolder.hasResource(RpcHolder.DEFAULT_USER_INFO)) {
            RpcHolder.unbindResource(RpcHolder.DEFAULT_USER_INFO);
          }
          //将用户信息加入缓存中
          RpcHolder.bindResource(RpcHolder.DEFAULT_USER_INFO, sessionUser);
        }
        //执行过滤操作
        chain.doFilter(request, response);
        Object serviceUser = RpcHolder.getResource(RpcHolder.DEFAULT_USER_INFO);
        if (RpcHolder.hasResource(RpcHolder.DEFAULT_USER_INFO)) {
          RpcHolder.unbindResource(RpcHolder.DEFAULT_USER_INFO);
        }
        //存入session
        session.setAttribute(RpcHolder.DEFAULT_USER_INFO, serviceUser);
        //这里一定要清除,不然会发生内存泄漏
        RpcHolder.remove();
      }
    }
    
    public abstract class RpcHolder {
    	// 该变量用于存放用户信息
    	public static final String DEFAULT_USER_INFO = "DEFAULT_USER_INFO";
    	//NamedThreadLocal是ThreadLocal子类,只是多个了name的属性。可以通过name获取
    	private static final ThreadLocal resources = new NamedThreadLocal("phprpc resources");
        //判断是否为空
      public static boolean hasResource(String key) {
    		Object value = doGetResource(key);
    		return value != null;
    	}
        //获取用户信息
    	public static Object getResource(String key) {
    		Object value = doGetResource(key);
    		return value;
    	}
    	private static Object doGetResource(String actualKey) {
    		Map map = (Map) resources.get();
    		if (map == null) {
    			return null;
    		}
    		Object value = map.get(actualKey);
    		return value;
    	}
        //绑定用户信息
        public static void bindResource(String key, Object value) throws IllegalStateException {
    		Map map = (Map) resources.get();
    		if (map == null) {
    			map = new HashMap();
    			resources.set(map);
    		}
    		map.put(key, value)
    	}
        //清空用户信息
        public static Object unbindResource(String key) throws IllegalStateException {
    		Map map = (Map) resources.get();
    		if (map == null) {
    			return null;
    		}
    		Object value = map.remove(actualKey);
    		if (map.isEmpty()) {
    			resources.set(null);
    		}
    		return value;
    	}
        public static void remove(){
            resources.remove();
        }
    }
    
    //获取用户信息工具类这个就是从ThreadLocal中获取
    public class UserUtils {
    	public static UserInfo getLoginUser() {
    		if (!RpcHolder.hasResource(RpcHolder.DEFAULT_USER_INFO)) {
    			throw new  InfoException("登录已超时,请重新登录!");
    		}
    		return (UserInfo) RpcHolder.getResource(RpcHolder.DEFAULT_USER_INFO);
    	}
    }
    

    简单说下思路,在每次请求过来我们会拦截请求把用户信息获取到后放到当前线程的ThreadLocal中,然后再处理业务的请求中就不需要从Session中获取,直接调用UserUtils方法从ThreadLocal获取即可。无论在Controller还是Service都可以获取不需要在传递用户对象。

  • 分布式系统链路追踪

    这里不过多赘述,MDC实现简单好用。底层也是用ThreadLocal实现。

    public class BasicMDCAdapter implements MDCAdapter {
        private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
    }
    
ThreadLocal底层实现:

先说说怎么做到每个线程只能读取到自己线程内的数据,数据隔离是怎么做到的。

public class TestSession {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    public static void main(String[] args) {
        threadLocal.set("a");
        threadLocal.get();
    }
}

看到get()/set()方法都是通过ThreadLocalMap map = getMap(t);获取到ThreadLocalMap然后ThreadLocalMap.Entry e = map.getEntry(this);然后操作的。

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
class Thread implements Runnable {
	/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

主要就是ThreadLocalMap我们需要关注一下,而ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的。这里我们基本上可以找到ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程ThreadthreadLocals变量里面的,别人没办法拿到,从而实现了隔离。

static class ThreadLocalMap {
	static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
}

ThreadLocalMap底层结构是Entry数组,Entry则是类似Mapkey(弱引用)-val键值对。

结构大致是这样的:(各自线程有个threadlocals是个数组结构)

在这里插入图片描述

这里可能会有疑惑,ThreadLocal不是只能保存一个Entry么?重复就会被覆盖么?为什么还需要用数组?

public class TestSession {
    private static ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();
    private static ThreadLocal<String> threadLocal2 = new ThreadLocal<String>();
    public static void main(String[] args) {
        threadLocal1.set("a");
        threadLocal2.set("a");
    }
}

这里要清楚一个线程只有一个threadlocals,但是可以有多个ThreadLocal所以每个ThreadLocal会占用一个数组节点。

那么又有人会问hash碰撞了怎么办?Map是用数组+链表解决,那这个怎么解决?

看看set()方法源码:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //计算下标
    int i = key.threadLocalHashCode & (len-1);
    /**
      * 指定下标被占用
      * for会从当前 i 下标开始循环遍历数组,直到数组单元为空退出循环
      * 这里也就是hash冲突之后会向后继续找有空位则入位或者是有相同的进行覆盖(e = tab[i = nextIndex(i, len))
      */
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //指定下标的threadlocal和set的一致,直接覆盖
        if (k == key) {
            e.value = value;
            return;
        }
        //表示threadLocal被gc回收(因为entry对threadLocal是弱引用),进行entry清理工作
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //当前下标没有被占用,直接在指定下标赋值
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

以下就是梳理的set()执行流程:

具体清理流程去看这篇博客,图文分析真的很全面。

https://blog.51cto.com/u_7592962/2543172

  1. 遍历当前key值对应的桶中Entry数据为空,这说明散列数组这里没有数据冲突,跳出for循环,直接set数据到对应的桶中跳到3.1
  2. 如果key值对应的桶中Entry数据不为空
    2.1 如果k = key,说明当前set操作是一个替换操作,做替换逻辑,直接返回
    2.2 如果key = null,说明当前桶位置的Entry是过期数据,执行replaceStaleEntry()方法(核心方法),然后返回
  3. for循环执行中在后迭代的过程中遇到了entrynull的情况则跳出循环
    3.1 在Entrynull的桶中创建一个新的Entry对象
    3.2 执行++size操作
  4. 调用cleanSomeSlots()做一次启发式清理工作,清理散列数组中Entrykey过期的数据
    4.1 如果清理工作完成后,未清理到任何数据,且size超过了阈值(数组长度的2/3),进行rehash()操作
    4.2 rehash()中会先进行一轮探测式清理,清理过期key,清理完成后如果size >= threshold - threshold / 4,就会执行真正的扩容逻辑

在这里插入图片描述

知道了怎么赋值那么取值就更简单了。

private Entry getEntry(ThreadLocal<?> key) {
    //计算下标,找到并且threadlocal也相等直接返回
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        //否则往后找
        return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    //玄幻往后找,找到返回。
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        //entry清理工作
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
内存泄漏:
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

因为继承WeakReference弱引用所以key是弱引用threadlocal

弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

那为什么不设置成强引用呢?

ThreadLocalMapkey为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。 譬如 设置:ThreadLocal = null 以后,应该会被回收的,但实际情况是ThreadLocalMap还有一个强引用,导致无法回收也会导致内存泄漏。

现在ThreadLocalMapkey为弱引用,设置:ThreadLocal = null 以后ThreadLocalMap是弱引用所以就可以回收。其实道理和为什么key为弱引用一致。

那怎么解决?

在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值