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的使用场景,这不是用来并发的,而是用来实现线程私有数据的保存。

 

posted @ 2018-02-26 15:28  Chaer  阅读(178)  评论(0编辑  收藏  举报