ThreadLocal的实现原理(读书笔记)
ThreadLocal的set方法和get方法,从set方法开始:
public void set(T value) { Thread t = Thread.currentThread();//获取当前线程 ThreadLocalMap map = getMap(t);//获取线程的局部变量 if (map != null)//判断map是否存在 map.set(this, value);//set值 key是当前ThreadLocal对象 value是value else createMap(t, value);//否则 创建一个map设置值 }
get方法:
public T get() { Thread t = Thread.currentThread();//获取当前线程 ThreadLocalMap map = getMap(t);//获取线程的局部变量map if (map != null) {//当map存在时 ThreadLocalMap.Entry e = map.getEntry(this);//获取entry(键值对) if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result;//返回值 } } return setInitialValue();//返回null }
在了解了ThreadLocal的内部实现后,我看到一个问题,那就是这些变量是维护在Thread类内部的,这也意味着只要线程不退出,对象的引用将一直存在,
当线程退出是,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap.下面是具体实现:在Thread类内部:
private void exit() { if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; /* Speed the release of some of these resources */ threadLocals = null; inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null; }
因此,如果我们使用线程池,那就意味着当前线程未必会退出,(比如固定大小的线程池,线程总是存在的,) 如果这样,将一些大大的对象设置到ThreadLocal中,可能会使系统出现内存在泄漏,
此时,你希望及时的GC,最好使用ThreadLocal.remove()方法将这个变量移除,就像我们习惯性的关闭数据库链接一样,如果你确定不需要这个对象了,那么就应该告诉虚拟机,把他回收掉,防止内存泄漏,
另外一种有趣的情况是JDK也可能允许你想释放普通变量一样释放ThreadLocal.比如,我么你有时候为了加入GC.会特意写出类似obj=null之类的代码.如果这么做,obj所指向的对象就会更容易地垃圾回收器发现,从而加速回收,
同理,如果对于ThreadLocal的变量,我们也手动将其设置null.比如t1=null.那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收,我们写个小例子来看一看奥秘:
public class ThreadLocalDemo_Gc { static volatile ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>() { @Override protected void finalize() throws Throwable { //重载了finalize() 当对象在GC时,打印信息 System.out.println(this.toString() + " is gc"); } }; static volatile CountDownLatch cd = new CountDownLatch(10000);//倒计时 public static class ParseDate implements Runnable { int i = 0; public ParseDate(int i) { this.i = i; } @Override public void run() { try { if (t1.get() == null) { t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") { @Override protected void finalize() throws Throwable { System.out.println(this.toString() + " is gc"); } }); System.out.println(Thread.currentThread().getId() + ":create SimpleDateFormat"); } Date t = t1.get().parse("2016-12-19 19:29:" + i % 60); } catch (ParseException e) { e.printStackTrace(); } finally { cd.countDown();//完成 计数器减1 } } } public static void main(String[] args) throws InterruptedException { ExecutorService es = Executors.newFixedThreadPool(10); for (int i = 0; i < 10000; i++) { es.execute(new ParseDate(i)); } cd.await();//等待所有线程 完成准备 System.out.println("mission complete!!"); t1 = null; System.gc(); System.out.println("first GC complete!!"); t1 = new ThreadLocal<>(); cd = new CountDownLatch(1000); for (int i = 0; i < 10000; i++) { es.execute(new ParseDate(i)); } cd.await(); Thread.sleep(1000); System.gc(); System.out.println("second GC complete!!"); } }
输出结果如下:
![](https://images2015.cnblogs.com/blog/939719/201612/939719-20161222180938073-302603740.png)
在主函数Main中,先后进行了2次任务提交,每次10000个任务,在第一次任务提交后,我们t1设置为null 接着进行了一次gc,接着我们进行了第二次任务提交,完成后在进行一次gc,
注意这些输出,.我们发现了当t1被设置为null时候,第一次gc 回收了.接着提交第二次任务,这次我们也是创建了10个线程,可以看到,虽然我们手动remove()这些对象,但是系统依然有可能回收他们.
要了解这里的回收机制,我们需要进一步了解Thread.ThreadLocalMap的实现,之前我们说过,ThreadLocalMap是一个类似HashMap的东西,更精确地说,他更加类似WeakHashMap.
ThreadLocalMap的实现使用了弱引用,弱引用是比强引用弱的多的引用,java在虚拟机回收时,如果发现若引用,就会立即回收,ThreadLocalMap内部由一系列Entry构成,每一个entry都是WeakReferenc<ThreadLocal>:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
这里的参数k就是map的key,v就是Map的value.其中k也就是ThreadLocal实例,作为弱引用使用(super(k)就是调用了WeakReferenc的构造函数,)因此,索然这里使用了ThreadLocal作为map的key,但是实际上,他并不是真的持有ThreadLocal的引用,而当THreadLocal的外部引用被回收时,ThreadLocalMap中的key就会变成null.当系统进行ThreadLocalMap清理时,就会自然将这些垃圾数据回收,