Java并发系列-ThreadLocal

156 阅读5分钟

Java并发系列-ThreadLocal

前言

面试过程中,并发知识相关中ThreadLoacl也是面试官爱问的一个点,小伙伴们一起看下下面这几个问题

  • ThreadLocal的原理是什么,他是如何解决并发访问相关问题的
  • ThreadLocal为什么会造成内存泄漏?如何解决内存泄漏问题,key为啥一定要使用弱引用
  • ThreadLocal的应用场景

原理

线程隔离

ThreadLocal里面有一个ThreadMap类型的变量threadLocals,ThreadMap我们简单理解就是个Map,key是线程对象本身,value是要存储的对象。

public T get() {
  //获取当前线程对象t
    Thread t = Thread.currentThread();
  //通过getMap方式获取到threadLoaclMap threadLocalMap保存所有的ThreadLocal变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      //map中存在值,返回Map中的值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
  //Map没有初始化,新建一个ThreadLocalMap对象
    return setInitialValue();
}
//新建ThreadLocalMap对象
 private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
          //不是空的添加到Map中
            map.set(this, value);
        else
          //空的话创建map
            createMap(t, value);
        return value;
    }    

ThreadLocalMap

查看相关源码可以得知,threadLocalMap和我们常使用的Map不大一样,这玩意没有实现map接口,而是通过Entry数组存储key,value的,我们一起来看下(查看源码的时候主要关注get方式和set方法)

ThreadLocalMap结构图

  static class ThreadLocalMap {
    //每一个Entry的key是一个弱引用。这样做的原因当变量key没有被其他多线使用的时候,自动回收ThreadLocal对象
     static class Entry extends WeakReference<ThreadLocal<?>> {
            //key是弱引用,value是强引用,这玩意你下次不退出,value一直存在(为甚这么设计?),
         		//key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景,我个人感觉方便垃圾回收
            //这个时候再get,set,remove都需要主动清理value,我们可以看下get方法
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
     //获取Map中的值,get方法
      private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
               //找到key,直接返回对于的value 
              return e;
            else
              //找不到,往后面继续查询顺道清理key为null的数据
              //为啥找不到还往后找,因为放元素的时候发现冲突的时候会继续往后面放
            return getEntryAfterMiss(key, i, e);
        }
    
      private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            while (e != null) {
              //从i位置开始遍历,寻找key能对应上的entry
                ThreadLocal<?> k = e.get();
                if (k == key)
                  //找到就返回
                    return e;
                if (k == null)
                  //key为null,说明弱引用key被回收,那就把value回收掉
                  //expungeStaleEntry 函数会从当前位置开始,往后再找一段,碰到脏entry进行清理,碰到null结束
                    expungeStaleEntry(i);
                else
                   //key不是要找的哪一个,出现hash冲突,处理冲突找下一个entry,继续往下寻找
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
    
       //往Map里面添加有一个值 set方法
		  private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //hash找到数据中的一个位置  
            int i = key.threadLocalHashCode & (len-1);
             //位置没有被占用,说明没有冲突,直接插入即可,不用for循环,如果位置被占用,就一直往下找到可用的位置
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
              ThreadLocal<?> k = e.get();
                if (k == key) {
                  //数组中的值等于threadLocal的key,将值进行替换即可
                    e.value = value;
                    return;
                }
                if (k == null) {
                  //key为空,说明key已经被回收,新key、value覆盖,同时清理掉旧的value值
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //将Entry放入tab中合适位置
            tab[i] = new Entry(key, value);
            int sz = ++size;
             //大于阈值,需要进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

应用场景

session管理

请求到来的时候,将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。但要记得当请求处理完成后通过remove方法将当前Session信息清除即可。

private static final ThreadLocal threadSession = new ThreadLocal();  
  
  public static Session getSession() throws Exception {  
    Session s = (Session) threadSession.get();   
        if (s == null) {  
            s = getSessionFactory().openSession();  
            //session请求。没有就放到ThreadLocal里面,线程之前隔离
            threadSession.set(s);  
        }  
    return s;  
}  
DateFormat线程安全

DateFormat是线程非安全的(多个线程之间共享变量calendar,并修改calendar),多线程情况下,必须为每一次日期转化创建一个DateFormate,这里

/**
* 同学感兴趣的话可以看下多线程下SimpleDateFormat会出现什么问题
*/
public class DateUtils {
    public static final ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
        //每个变量线程副本
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}
//调用
DateUtils.df.get().format(new Date());

问题解答

通过读源码我们一起总结下上面提到的几个问题回答的点

  • ThreadLocal的原理是什么,他是如何解决并发访问相关问题的
    • map线程隔离,保留副本
  • ThreadLocal为什么会造成内存泄漏?如何解决内存泄漏问题,key为啥一点要使用弱引用
    • 因为entry的key为弱引用,解决就是每次使用完ThreadLocal,都调用它的remove()方法,清除数据
    • 为啥使用弱引用
      • 如果key 使用强引用:引用的ThreadLocal的对象被回收了,但entry没有被回收,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
      • 如果key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除
      • 总结就是ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除
  • ThreadLocal的应用场景
    • session管理
    • DateFormat线程安全

闲谈

感觉有帮助的同学还请点赞关注,这将对我是很大的鼓励~,公众号有自己开始总结的一系列文章,需要的小伙伴还请关注下个人公众号程序员fly呀,干货多多,湿货也不少(∩_∩)。

巨人肩膀

juejin.cn/post/695933…

blog.csdn.net/m0_50180963…

blog.csdn.net/vking_wang/…

www.pdai.tech/md/java/thr…