ThreadLocal源码解析

最近面试关于ThreadLocal的问题竟被一面和二面的面试官同时问了。问怎么实现的?以前都是知道怎么用,没看过源码。所以没回答上来,感觉在这种低级的问题上丢分很不值当,所以抽空看了一下ThreadLocal的源码。记录下来,加深印象。言归正传。

ThreadLocal 即线程本地变量。即每个线程持有一个变量的副本,线程对变量的操作只针对于变量值的副本。ThreadLocal和同步锁都是用来实现多个线程对同一个变量的安全访问。用同步锁机制的时候,变量只有一个,只不过是通过锁的作用让多个线程串行的去访问变量。Threadlocal是从另一个角度来解决多线程下并发访问变量不安全的问题,ThreadLocal将需要并发访问的资源复制出多份来,每个线程都拥有自己独立的副本,从而就不存在不安全访问的问题了。

ThreadLocal并不能代替同步锁,俩者面向问题的领域不同。同步锁是为了多个线程对于资源的并发访问做的控制,资源对每个线程而言是同步。而ThreadLocal是复制多份资源,资源在每个线程中是不同步。

通常如果需要进行多个线程之间共享资源,以达到线程之间通信的功能,就是用同步锁;如果仅仅需要隔离多个线程之间的共享冲突,可以使用ThreadLocal。

简单的demo:

public class Test5 {
    public static void main(String[] args) {
        //创建一个ThreadLocal对象
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        //赋值
        threadLocal.set("Hello");
        //获取
        String str = threadLocal.get();
        //输出Hello
        System.out.println(str);
        //移除
        threadLocal.remove();
        //再次获取
        str = threadLocal.get();
        //输出null
        System.out.println(str);
    }
}

以下代码可以创建一个包含初始化值的ThreadLocal对象

ThreadLocal<String> threadLocal = new ThreadLocal(){
            @Override
            public String initialValue(){
                return "defalut";
            }
        };

可以看到ThreadLocal核心就三个方法:

T get()
void set(T value)
void remove()

下面就这三个方法我们一起看看源代码,也没啥神秘的,不知道为什么面试官爱问!!!!

只看set方法源码即可!set看懂了get和remove基本不用看了!

//ThreadLocal.java
//代码块1
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.threadLocals;
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

可以看到在Thread类有一个名为:threadLocals,类型为:java.lang.ThreadLocal.ThreadLocalMap类型的成员变量。申明如下:

//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

【代码块1】第一次set的时候threadLocals为null,所以会走else ,下面看看createMap(t, value)方法里都干了什么?

t.threadLocals = new ThreadLocalMap(this, firstValue);

可以看到就是new 了一个ThreadLocalMap 赋给 Thread对象的threadLocals变量。ThreadLocalMap如下:

//ThreadLocal.ThreadLocalMap.java
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
}

        到这儿基本上能看到ThreadLocal的存储形式了!在捋一下,在Thread类有个名为threadLocals,类型为ThreadLocalMap类型的成员变量,ThreadLocalMap内部有个Entry数组,Entry对象的key是ThreaLocal自己,value则是往ThreadLocal里存的对象。

开始new出一个大小为16的Entry[],根据ThreadLocal对象的threadLocalHashCode和Entry[]大小计算entry对象在Entry[]中的存储下标。最后一步则是设置Entry[]数组扩容的阀值。阀值的意思是:Entry[]的实际使用率超过Entry[]长度的2/3的时候重新rehash扩容。

下面需要重点关注以下这个Entry对象,看源码:

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

可以看到Entry 继承了 WeakReference对象,关于WeakReference(弱引用)不了解的可以看看博主的另一篇文章《五分钟搞明白JAVA的软引用,弱引用,虚引用》,那么继承WeakReference后Entry会有什么样的特性呢?为什么要继承WeakReference呢?请注意记住下面标红的话,后面说ThreadLocal会不会有内存溢出的问题时候会用到。当ThreadLocal对象失去引用强应用的时候,Entry.get() 会返回null,但是Entry.value还被Entry对象引用的。因为Entry继承WeakReference,所以Entry所引用ThreadLocal当失去强引用的后,会被GC回收掉。所以Entry.get ()会返回null。

以上代码都是第一次调用ThreaLocal.set的时候进入else分支的时候,那现在看看第二次及以后调用ThreaLocal.set方法,即Thread.threadLocals 不等于null的时候,即进入(代码块1)的if分支。有是怎么做的呢,看源码:

private void set(ThreadLocal key, Object value) {
   Entry[] tab = table;
   int len = tab.length;
   //tag-1
   int i = key.threadLocalHashCode & (len-1);
   //tag-2
   for (Entry e = tab[i]; e != null;  e = tab[i = nextIndex(i, len)]) {
          ThreadLocal k = e.get();
          //tag-3
          if (k == key) {
                e.value = value;
                return;
          }
          //tag-5
          if (k == null) {
                //tag-5
                replaceStaleEntry(key, value, i);
                return;
          }
   }
   //tag-6
   tab[i] = new Entry(key, value);
   int sz = ++size;
   //tag-7   
   if (!cleanSomeSlots(i, sz) && sz >= threshold){
       //tag-8
       rehash();
   }
}

这段代码可能看着比较绕,别急!我们一行一行的来分析。

未完待续!太晚了。。。

---------------------------继续下班没事干了继续写写(2018.08.14 19:00)

tag-1计算Entry[] 数组的下标,tag-2的处循环判断e是否为空,为空就表示Entry[i]是为被使用的,结束循环到tag-6 new 处一个Entry对象填充到Entry[i]的位置,紧接着++size。这里的size表示Entry[]的实际使用个数,即在当前线程上一共new 出了多个少ThreadLocal对象。tag-7处的 cleanSomeSlots方法是干什么用的呢?看代码:

private boolean cleanSomeSlots(int i, int n) {
   boolean removed = false;
   Entry[] tab = table;
   int len = tab.length;
   do {
       i = nextIndex(i, len);
       Entry e = tab[i];
       if (e != null && e.get() == null) {
           n = len;
           removed = true;
           i = expungeStaleEntry(i);
       }
       } while ( (n >>>= 1) != 0);
   return removed;
}

其实根据名字就能判断出来这个一个释放已经失去引用的Entry对象。如果成功释放了部分Entry对象就返回true,没有Entry被释放就返回false。tag-7处判断:如果没有成功释放调Entry对象 且 size 大于 扩容阀值 threshold,则执行tag-8,扩容Entry[]数组。

接下来看看什么时候会进入tag-3处的if 体呢?即重复调用 ThreadLocal.set方法的时候,因为这个时候对应的Entry对象已经创建,重复调用set只是在不停的替换Entry的value属性而已。

那么什么时候会进入tag-5处的if 体呢?当另一个ThreadLocal对象失去强引用的时候,ThreadLocal对象被GC掉了,对应的Entry对象的get方法会返回null。所以if体里用当前的ThreadLocal和value生成新的Entry对象替换Entry[i]处失去引用的ThreadLocal对象对应的Entry对象,从而让已经失去引用的ThreadLocal对象引用的value失去引用。可以GC回收掉过期的Value,防止内存溢出和泄漏。

这里大家要理解,当new 一个ThreadLocal对象,调用set方法后会生成一个Entry对象存放到一个Entry[]数组中。当这个ThreadLocal失去强引用后,ThreadLocal对象会被GC掉,Entry.get 方法会返回null,但是对应的Entry对象还是被Entry[] 所引用,并不会被GC的。那么什么时候这个Entry会被GC呢?三种情况:

1.当前线程运行结束,即Thread对象失去引用(要注意tomact,jetty,线程池一般不会轻易的销毁一个线程)。

2.主动调用ThreadLocal对象的remove方法。

3.在同一个线程上调用别的ThreadLocal的set方法自动清理失去强引用的ThreadLocal对象关联的Entry对象。

        看看下面代码的

public static void main(String[] args) {
   Thread thread_dmeo = new Thread(new Runnable() {
       @Override
       public void run() {
           ThreadLocal<String> threadLocal_s = new ThreadLocal<>();
           threadLocal_s.set("hello");
         }
       });
   thread_dmeo.start();
}

大概内存引用如上图,Thread对象引用着一个Entry[]数组,Entry保持着对ThreadLocal对象弱引用。thread_demo线程死亡,则所有相关对象都会释放。若threadlocal_s失去对ThreadLocal对象的引用后,GC会回收ThreadLocal对象,下次往Entry[]添加Entry对象的时候,会自动遍历Entry[]数组,调用Entry.get()方法返回null, 系统就会断开Entry与“Hello”的强引用。故下次GC的时候“Hello”会被回收掉。

故如果往ThreadLocal里set对象后,一直没有调用remove方法,会不会有内存溢出和泄漏的隐患呢?

个人认为不会有内存溢出的问题,泄漏更谈不上了。因为ThreadLocal每次set会自动回收失去引用相关对象。但是在使用线程池或Tomcat等web容器的时候,因为线程复用的原因造成获取保存在ThreadLocal中的数据时出现脏读的问题。但这不是ThreadLocal本身的问题。因为如果不及时调用remove方法释放的资源的话,会有内存回收不及时,脏读等隐患,所以还是建议使用后养成remove的习惯。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值