ThreadLocal解析

ThreadLocal含义

首先明确ThreadLocal并不是解决多线程下共享对象的并发访问而产生的,他也做不到这一点,因为他内部保存的仍然是对象的引用,而不是真的存储的是对象本身,通常使用ThreadLocal保存的对象,是当前线程单独创建(new)的对象,然后通过ThreadLocal.set()放入到当前当前线程中,这样对该对象的操作肯定不会影响到其他线程,其他线程也不需要对该对象进行操作(其他线程想要应该要自己去创建属于该线程自己的对象)
下面代码证明:ThreadLocal存储的是对象引用,无法保证共享对象的线程安全

public class ThreadLocalTest {
    	public static void main(String[] args) {
          final Student aa = new Student();
    	    aa.setName("初始名称");
    	    new Thread(new Runnable() {
                public void run() {
                    ThreadLocal<Student> threadLocal1 = new ThreadLocal<Student>();
                    threadLocal1.set(aa);
                    System.out.println("线程一:"+threadLocal1.get().getName());
                    threadLocal1.get().setName("被线程一修改后的名称");
                }
            }).start();

    	    new Thread(new Runnable() {
                public void run() {
                    ThreadLocal<Student> threadLocal2 = new ThreadLocal<Student>();
                    threadLocal2.set(aa);
                    try {
                        Thread.sleep(2000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程二:"+threadLocal2.get().getName());
                }
            }).start();

        }
}
class Student{
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

运行上面程序获取的结果是:

线程一:初始名称
线程二:被线程一修改后的名称

上面的程序含义是创建一个共享的Student对象,创建两个线程分别在线程内部使用ThreadLocal保存该对象,线程一保存完毕后修改该对象的属性,根据程序运行结果可以看出线程二中保存的Student对象名称也被修改,因此ThreadLocal不能保存或者说不应保存共享对象,他应该只保存当前线程私有(本地)变量

ThreadLocal的实现(参考的JDK1.7的源码)

set()方法

ThreadLocal保证在当前线程中创建的对象,通过ThreadLocal.set()放入到当前当前线程中,通过ThreadLocal.get()获取存入的值,其他线程无法通过ThreadLocal.get()获取当前线程存入的值,这里的实现可以通过查看ThreadLocal的set/get方法源码查看

public void set(T value) {
     Thread t = Thread.currentThread();
     //获取当前线程的ThreadLocalMap
     ThreadLocalMap map = getMap(t);
     if (map != null)
        //存在就直接往里面塞值
         map.set(this, value);
     else
        //不存在,就创建一个,然后设值
         createMap(t, value);
 }
  1. 每一个线程都有一个ThreadLocal.ThreadLocalMap类型的成员变量,这里获取当前线程对应的ThreadLocalMap,他存储的也是key-value类型的数据结构,这里如果不理解ThreadLocalMap的结构可以先将ThreadLocalMap简单看成HashMap,后面具体分析ThreadLocalMap的结构
  2. 如果当前线程有ThreadLocalMap,将ThreadLocal对象本身作为键,要存的值作为值存放到ThreadLocalMap中,没有就创建一个ThreadLocalMap然后存放键及值

ThreadLocalMap结构及深入set()/get()方法

ThreadLocalMap的结构和HashMap类似,如下图所示:

mark

ThreadLocalMap内部同样有一个初始大小为16的Entry数组,他和HashMap中的Entry区别是

  1. Entry类继承WeakReference,他对键的引用是弱引用
  2. 他的数据结构里面没有链表,发生哈希冲突后会根据key判断更新当前位置值还是寻找下一个位置

关于第一点区别可以参考Entry类的源码

static class Entry extends WeakReference<ThreadLocal> {
       Object value;
       Entry(ThreadLocal k, Object v) {
           super(k);
           value = v;
       }
   }

从源码上可以看出Entry本质上是WeakReference,具体来说是Entry实例对ThreadLocal的某个实例是弱引用,同时还持有value的强引用

关于第二点区别可以参考源码详细分析

private void set(ThreadLocal key, Object value) {

      Entry[] tab = table;
      int len = tab.length;
      //确定本次存储在Entry数组的索引位置
      int i = key.threadLocalHashCode & (len-1);

     /*
      *对于本次要存储的键值对应位置已有值,则说明发生了hash冲突
      *发生hash冲突的解决方法是:遍历整个数组,直到找到key或者是找到空位
      */
      for (Entry e = tab[i];
           e != null;
           e = tab[i = nextIndex(i, len)]) {
          ThreadLocal k = e.get();
          //找到了该Entry并且键值相等,新值换旧值
          if (k == key) {
              e.value = value;
              return;
          }
          //该位置key已经被gc回收,
          if (k == null) {
              replaceStaleEntry(key, value, i);
              return;
          }
      }
      //未发生hash碰撞直接赋值即可
      tab[i] = new Entry(key, value);
      int sz = ++size;
      /*
       *添加数据后做一下是否扩容的判断
       *1.没有要清理的被gc回收key的Entry
       *2.满足容量扩容条件
       */
      if (!cleanSomeSlots(i, sz) && sz >= threshold)
          rehash();
  }

从源码上set()步骤就是

  1. 确定本次存储的键值在Entry数组中的位置
  2. 看是否发生Hash冲突,如果冲突了(该位置已经有Entry对象了),看该位置的Entry的的key是否为null,为null说明该键已被gc回收,将现有的键值插入即可,不为null查看已经存在的key和本次要插入的key是否相同,如果相同,更新值即可
  3. 未发生冲突直接构造新的键值Entry存到数组中即可
  4. 判断是否需要扩容

ThreadLocal的应用场景

  1. hibernate数据库链接池,spring的事务实现
  2. 多层间的参数传递
  3. 某些情况提升安全和性能(DateFormat工具类)

ThreadLocal的问题-内存泄漏

所谓ThreadLocal的内存泄漏主要体现在线程池的环境下,Entry中的value无法被垃圾回收器回收,因为有一条强引用链Thread->ThreadLocalMap->Entry->某个Entry中的value,正常情况下当线程执行完销毁后这条引用链就不会存在了,但线程池环境下线程不会被销毁,因此也就造成所谓的内存泄漏,ThreadLocal的源码中也做了一定的措施防范内存泄漏,就是对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。但是这是有概率的,因此线程池环境下,我们编码要注意用完调用remove()方法

参考链接

ThreadLocal源码解读
ThreadLocal 内部实现、应用场景和内存泄漏
ThreadLocal 内存泄露的实例分析
清理ThreadLocal

posted @ 2018-04-12 14:14  柠檬请问  阅读(139)  评论(0编辑  收藏  举报