ThreadLocal为什么使用WeakReference

转自:http://www.voidcn.com/article/p-zgtnlfcs-ng.html

说这里和弱引用有什么关系了。因为这个ThreadLocalMap并非用户定义,用户只是new了一个ThreadLocal对象,所以当用户定义的ThreadLocal对象不再使用之后,ThreadLocal对象及其指向的T对象都应该可以被回收。可是由于很多线程中的ThreadLocalMap对象中保存了ThreadLocal对象和T对象的Entry(键值对)中的ThreadLocal对象回收可能会受到阻碍。因此这里看代码可以看到ThreadLocal对象作为key的时候首先被WeakReference包裹了一下的,如下。

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

那么说,Entry声明为WeakReference,岂不是说,在某个任意时刻,GC都有可能把ThreadLocal对象对应的Value也给回收了?

答案是No!因为我们声明的WeakReference引用指向的是ThreadLocal对象,而value则是正正经经的强引用类型好不好??

可以看到这里的Entry类的k被做了弱引用,即ThreadLocal对象回收不会被这里的Entry所影响。效果就是在新的ThreadLocal执行set方法的时候,遍历map中的table时可能会发现好多Entry.get()为空的Entry,即key为空。这说明这条记录已经可以呗删掉了,这个时候就可以用新的Entry替换掉旧的。看一下代码吧。如下。

private void set(ThreadLocal key, Object value) {
                  // We don't use a fast path as with get() because it is at
                  // least as common to use set() to create new entries as
                  // it is to replace existing ones, in which case, a fast
                  // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                        ThreadLocal k = e.get();
                        if (k == key) {
                              e.value = value;
                              return;
                        }
                        if (k == null) {
                              replaceStaleEntry(key, value, i);
                              return;
                        }
                  }

                  tab[i] = new Entry(key, value);
                  int sz = ++size;
                  if (!cleanSomeSlots(i, sz) && sz >= threshold)
                        rehash();

}

其中if(k == null)段就是做这个用的。如果这里不使用WeakReference则无法做到这点

 

WeakReference是Java语言规范中为了区别直接的对象引用(程序中通过构造函数声明出来的对象引用)而定义的另外一种引用关系。WeakReference标志性的特点是:reference实例不会影响到被应用对象的GC回收行为(即只要对象被除WeakReference对象之外所有的对象解除引用后,该对象便可以被GC回收),只不过在被对象回收之后,reference实例想获得被应用的对象时程序会返回null。

理解了WeakReference之后,ThreadLocalMap使用它的目的也相对清晰了:当threadLocal实例可以被GC回收时,系统可以检测到该threadLocal对应的Entry是否已经过期(根据reference.get() == null来判断,如果为true则表示过期,程序内部称为stale slots)来自动做一些清除工作,否则如果不清除的话容易产生内存无法释放的问题:value对应的对象即使不再使用,但由于被threadLocalMap所引用导致无法被GC回收。实际代码中,ThreadLocalMap会在set,get以及resize等方法中对stale slots做自动删除(set以及get不保证所有过期slots会在操作中会被删除,而resize则会删除threadLocalMap中所有的过期slots)。当然将threadLocal对象设置为null并不能完全避免内存泄露对象,最安全的办法仍然是调用ThreadLocal的remove方法,来彻底避免可能的内存泄露。

 

ThreadLocal的目的就是为每一个使用ThreadLocal的线程都提供一个值,让该值和使用它的线程绑定,当然每一个线程都可以独立地改变它绑定的值。如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程序.

关于的ThreadLocal更多内容,请参考《ThreadLocal》。

在阅读了ThreadLocal的源码后,我发现如果我们使用不恰当,可能造成内存泄露。经我测试,内存泄露的确存在。虽然该内存泄露,理论上上已经不算严重。

测试代码如下

ThreadLocalTest文件

package com.teleca.robin;
public class ThreadLocalTest {
 public ThreadLocalTest()
 {
 }

 ThreadLocal<Content> tl=new ThreadLocal<Content> ();
 void start(){
          System.out.println("begin");
          Content content=tl.get();
          if(content==null) {
               content= new Content();
               tl.set(content);
      }

  System.out.println("try to release content data");
  //tl.set(null);//@1
  //tl.remove();//@2
  tl=null;//@3
  content=null;//@4
  System.out.println("request gc");
  System.gc();
  try {
   Thread.sleep(1000);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  System.out.println("end");
 }
}

class Content{
 byte data[]=new byte[1024*1024*10];
 protected void finalize() {
  System.out.println("I am released");
 }

}

运行结果

begin

try to release content data

request gc

end

注意我们尝试在@3和@4处,释放对tl和content的引用,以便JAVA虚拟机回收content。但是测试结果表明还有对content的引用,以致它没有能被JAVA虚拟机回收。因为线程没有结束,Thread对象还在,所以value没有被回收(强引用类型哦!!)

我们必须把@1或@2处的代码打开,才能把让它让JAVA虚拟机回收content.推荐打开@2而不是@1

把@1或@2处的代码打开后的运行结果如下:

begin

try to release content data

request gc

I am released

end

另外注意,@3其实并不影响运行结果。

 

 

事实上每个Thread实例都有一个ThreadLocalMap成员变量,它以ThreadLocal对象为key,以ThreadLocal绑定的对象为Value。

.我们调用ThreadLocal的set()方法,只是把要绑定的对象存放在当前线程的ThreadLocalMap成员变量中,以便下次通过get()方法取得它。

ThreadLocalMap和普通map的最大区别就是它的Entry是针对ThreadLocal弱引用的,即当ThreadLocal没有其他引用为空时,JVM就可以GC回收ThreadLocal,从而得到一个null的key。

关于ThreadLocalMap的更多内容请参考《为ThreadLocal定制的ThreadLocalMap》

 

 

ThreadlocalMap维护了ThreadLocal对象和其绑定对象之间的关系,这个ThreadLocalMap有threshold,当超过threshold时,

ThreadLocalMap会首先检查内部ThreadLocal引用(前文说过,ThreadLocal是弱引用可以释放)是否为null,如果存在null,那么把绑定对象的引用设置为null,以便释放ThreadLocal绑定的对象,这样就腾出了位置给新的ThreadLocal。如果不存在slate threadlocal,那么double threshold。

除此之外,还有两个机会释放掉已经废弃的ThreadLocal绑定的对象所占用的内存,

一、当hash算法得到的table index刚好是一个null 的key的threadlocal时,直接用新的ThreadLocal替换掉已经废弃的。

二、每次在ThreadLocalMap中存放ThreadLocal,hash算法没有命中既有Entry,需要新建一个Entry时,也调用cleanSomeSlots来遍历清理Entry数组中已经废弃的ThreadLocal绑定的对象的引用。

此外,当Thread本身销毁时,这个ThreadLocalMap也一定被销毁了(ThreadLocalMap是Thread对象的成员),

这样所有绑定到该线程的ThreadLocal的Object Value对象,如果在外部没被引用的话(通常是这样),也就没有任何引用继续保持,所以也就被销毁回收了。

 

 

从上可以看出Java已经充分考虑了时间和空间的权衡,但是因为置为null的ThreadLocal对应的Object Value在无外部引用时,任然无法及时回收。

ThreadLocalMap只有到达threshold时或添加entry时才做检查,不似gc是定时检查,

不过我们可以手工通过ThreadLocal的remove()方法或set(null)解除ThreadLocalMap对ThreadLocal绑定对象的引用,及时的清理废弃的threadlocal绑定对象的内存以。remove()往往还能做更多的清理工作,因此推荐使用它,而不使用set(null).

需要说明的是,只要不往不用的threadlocal中放入大量数据,问题不大,毕竟还有回收的机制。

 

 

被废弃了的ThreadLocal所绑定对象的引用,会在以下4情况被清理。

如果此时外部没有绑定对象的引用,则该绑定对象就能被回收了:

1 Thread结束时。

2 当Thread的ThreadLocalMap的threshold超过最大值时。

3 向Thread的ThreadLocalMap中存放一个ThreadLocal,hash算法没有命中既有Entry,而需要新建一个Entry时。

4 手工通过ThreadLocal的remove()方法或set(null)。

 

 

因此如果我们粗暴的把ThreadLocal设置null,而不调用remove()方法或set(null),那么就可能造成ThreadLocal绑定的对象长期也能被回收,因而产出内存泄露。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值