ThreadLocal 源码分析
1、个人总结和想法:
(1)、ThreadLocal的内存泄漏问题?
ThreadLocal 我们应该关注它的内存泄漏问题,原因虽然JDK开发者已经使用了弱引用的键来尝试解决这个问题,不过是依然存在很大风险的,因为当使用static的ThreadLocal时会使其生命周期和类一样,这样是没有必要的,就造成了内存泄漏,还有我们使用完了没有及时的清除ThreadLocal也会导致内存泄漏,内存泄漏就是让本应该被回收的对象还依然占用着内存。
(2)ThreadLocal中的对象关系?
ThreadLocal内部有一个静态内部类ThreadLocalMap,而每一个线程对象都有一个ThreadLocalMap的属性,ThreadLocalMap可以理解为一个Map,只是它的键很特殊,线程使用定义的ThreadLocal作为ThreadLocalMap的键,并且ThreadLocal是一个弱引用,这就与之前的相呼应了。还有就是每一个线程对象定义了几个ThreadLocal它就只能保存几个线程私有属性,我觉得这并不很优雅,但是鉴于ThreadLocalMap的数据结构,我只能这么想。
(3)线程池为什么不能用ThreadLocal?
这个道理显而易见,因为我们的线程池里面的线程是要复用的,也许这条线程在处理你的任务,下一时刻就要处理别人的任务,所以使用ThreadLocal没有意义,ThreadLocal本来就是想做到线程隔离,只为自己服务,而不是为了共享数据,而且还很可能被覆盖。
2、源码分析:
重要方法分析:
setInitialValue()源码分析:
1 private T setInitialValue() { 2 //设置默认值 为null 3 T value = initialValue(); 4 //获取当前线程 5 Thread t = Thread.currentThread(); 6 //获取map 7 ThreadLocalMap map = getMap(t); 8 if (map != null) 9 map.set(this, value); 10 else 11 //初始化创建map 12 createMap(t, value); 13 return value; 14 }
上面的是设置entry的value默认值为null
createMap()源码:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
创建ThreadLocalMap并初始化Map
ThreadLocal set()源码:
1 public void set(T value) { 2 //获取当前线程 3 Thread t = Thread.currentThread(); 4 //根据当前线程获取map 因为ThreadLocalMap是每一个线程都有的属性 5 ThreadLocalMap map = getMap(t); 6 if (map != null) 7 //设置值 8 map.set(this, value); 9 else 10 //初始化map 11 createMap(t, value); 12 }
set()源码很简单 就是简单的设置ThreadLocalMap
ThreadLocal get()源码:
1 public T get() { 2 //获取当前线程 3 Thread t = Thread.currentThread(); 4 //获取map 5 ThreadLocalMap map = getMap(t); 6 if (map != null) { 7 //得到entry 8 ThreadLocalMap.Entry e = map.getEntry(this); 9 if (e != null) { 10 @SuppressWarnings("unchecked") 11 T result = (T)e.value; 12 return result; 13 } 14 } 15 //返回默认值 16 return setInitialValue(); 17 }
getEntry()源码:
1 private Entry getEntry(ThreadLocal<?> key) { 2 //得到索引 3 int i = key.threadLocalHashCode & (table.length - 1); 4 Entry e = table[i]; 5 //找到返回 6 if (e != null && e.get() == key) 7 return e; 8 else 9 //没找到继续找 10 return getEntryAfterMiss(key, i, e); 11 }
解释:因为ThreadLocalMap和HashMap不一样,解决hash碰撞的方式不是链地址法而是开放地址法,这样可以让空间效率的到比较明显的提升
ThreadLocal remove()源码:
1 public void remove() { 2 //获取map 3 ThreadLocalMap m = getMap(Thread.currentThread()); 4 //如果不为空 就移除 5 if (m != null) 6 m.remove(this); 7 }
上面是分析的ThreadLocal的方法 很简单是不是,那是因为封装了ThreadLocalMap的方法。下面我们来看真正起作用的。
ThreadLocalMap最重要的数据结构Map
entry的源码:
1 //键继承了弱引用 2 static class Entry extends WeakReference<ThreadLocal<?>> { 3 //value值 4 Object value; 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k); 7 value = v; 8 } 9 }
每一个节点都是一个entry 是一个键值对,键为ThreadLocal本身,值为用户需要保存的value。
ThreadLocalMap的set()源码:
1 private void set(ThreadLocal<?> key, Object value) { 2 //entry数组 3 Entry[] tab = table; 4 int len = tab.length; 5 //获取索引 6 int i = key.threadLocalHashCode & (len-1); 7 //下面就是往entry数组里面放值了 8 for (Entry e = tab[i]; 9 e != null; 10 e = tab[i = nextIndex(i, len)]) { 11 ThreadLocal<?> k = e.get(); 12 13 if (k == key) { 14 e.value = value; 15 return; 16 } 17 18 if (k == null) { 19 replaceStaleEntry(key, value, i); 20 return; 21 } 22 } 23 24 tab[i] = new Entry(key, value); 25 int sz = ++size; 26 if (!cleanSomeSlots(i, sz) && sz >= threshold) 27 rehash(); 28 }
解释:因为我们之前说过是通过开放地址法来解决冲突,所以大致的逻辑是先获取索引位置,如果不为空且相等就直接覆盖,如果为空就表示可能键已经被GC回收了,这时候就会清除这个entry,如果不为空且不相等就往后面数组移动直到找到。
getEbtryAfterMiss()源码:
1 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { 2 Entry[] tab = table; 3 int len = tab.length; 4 5 while (e != null) { 6 ThreadLocal<?> k = e.get(); 7 //找到直接返回 8 if (k == key) 9 return e; 10 if (k == null) 11 //如果键已经被回收了就执行后面的逻辑 清除这个entry 12 expungeStaleEntry(i); 13 else 14 //存在但不相等就寻找下一个entry 比较 15 i = nextIndex(i, len); 16 e = tab[i]; 17 } 18 //没有找到就返回null 19 return null; 20 }
总结:ThreadLocal的set方法实际上就是调用了ThreadLocalMap的set方法 ,核心方法是expungeStaleEntry()和replaceStaleEntry()这两个方法实际上会检查entry数组的键即ThreadLocal有没有被回收,因为弱引用是很容易被回收的,所以相当于JDK设计者也想通过者来一定程度上防止内存泄漏。对了 内存泄漏,是指的是Entry泄漏,这是我和一位朋友的观点,很多人说是value泄漏,value只要有引用就不算泄漏,enrty它的键都被回收了,就应该被回收了,所以真正泄漏的是entry。
ThreadlocalMap的remove方法:
1 private void remove(ThreadLocal<?> key) { 2 //获取entry数组 3 Entry[] tab = table; 4 int len = tab.length; 5 //获取下标索引 6 int i = key.threadLocalHashCode & (len-1); 7 for (Entry e = tab[i]; 8 e != null; 9 e = tab[i = nextIndex(i, len)]) { 10 if (e.get() == key) { 11 //清除value 12 e.clear(); 13 //清除entry 14 expungeStaleEntry(i); 15 return; 16 } 17 } 18 }
同理可得 ThreadLoca的remove实际上也是ThreadLocalMap在实际处理。
使用ThreadLocal的建议:
使用完就手动remove ,虽然get() set()方法也会检测是不是ThreadLocal是否为null,不过你最好使用完清除,因为巧合可能会让你的系统遇到麻烦
尽量不要用static来修饰ThreadLocal ,一般的业务场景是不需要的
搞清楚ThreadLocal的使用场景,这不是用来并发的,而是用来实现线程私有数据的保存。