ThreadLocal 详解
一、介绍
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将 ThreadLocal 类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
再举个简单的例子:
比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
二、ThreadLocal的存储结构
前面说了 ThreadLocal 就是一个专为单个线程存储的值,不会受其他线程的影响,而我们现在想问,这个值存在哪里?如何去取用?
每个线程是有自己存储数据的数据栈,而这个栈只有自己可见,而堆则是所有线程都可见,所以既然有了这个特点,那 ThreaLocal 对应的值是存在栈中的么,答案是否定的,其实值还是存在于堆中,只不过利用了一些手段使得只有对应线程可见
那如何存,如何取?
ThreadLocal的数量可能有多个,而每一个ThreadLocal对应的是不同的值,所以线程利用了类似 HashMap 的结构(不是完全一样),在一个线程中有 ThreadLocalMap
来存储,可以看一下源码:
1 public class Thread implements Runnable { 2 //...... 3 //与此线程有关的ThreadLocal值。由ThreadLocal类维护 4 ThreadLocal.ThreadLocalMap threadLocals = null; 5 6 //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 7 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 8 //...... 9 }
这是存储数据的地方,初始值都为null,当我们用ThreadLocal调用了get()或者set()方法后就会分配内存,我们接下来看一下 get() 和 set() 方法
get()方法:
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 ThreadLocalMap.Entry e = map.getEntry(this); 6 if (e != null) { 7 @SuppressWarnings("unchecked") 8 T result = (T)e.value; 9 return result; 10 } 11 } 12 return setInitialValue(); 13 }
set()方法:
1 public void set(T value) { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 map.set(this, value); 6 } else { 7 createMap(t, value); 8 } 9 }
如果读懂了上述 get() 和 set() 方法,会发现 Map 存储在线程之上,在 ThreadLocal 中通过 Thread.currentThread()来得到此线程,然后通过这个线程来获取这个线程对应的 Map ,而Map上的数据存储方式采用的是 <key,value> ,其中 key 的值是 ThreadLocal 自己,而value的值是我们想要获取的值,如下图所示:
可以看出这个存储结构与HashMap极为相似,但还是有区别的,区别的重点就在于 HashMap 利用拉链法来防止 Hash 冲突,这个结构没有链表,如何防止冲突呢?
其实它用了另外一种避免 Hash 冲突的方法:开放地址法中的线性探索法:当存入数据出现 Hash 冲突的时候,指针就会在 Hash 冲突的位置采用 +1 或者 -1 的操作,直到找到空余位置,就会将数据存入,这样就避免的 Hash 冲突。
因为 Key 值 ThreadLocal 一般不会很多,所以用这种方法的查询效率在这种情况下也是可以保证的
三、深入理解ThreadLocal
1、如何让其他线程可以访问 ThreadLocal 的值?
这里我们首先回到前面的代码:
1 public class Thread implements Runnable { 2 //...... 3 //与此线程有关的ThreadLocal值。由ThreadLocal类维护 4 ThreadLocal.ThreadLocalMap threadLocals = null; 5 6 //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 7 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 8 //...... 9 }
我们会发现在创建 ThreadLocals 的同时,创建了一个 inheritableThreadLocals ,这个就是问题的关键,我们继续看给这个变量什么样的值:
1 public class Thread implements Runnable { 2 …… 3 if (inheritThreadLocals && parent.inheritableThreadLocals != null) 4 this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 5 …… 6 }
我们发现当 inheritThreadLocals 和 parent.inheritableThreadLocals(父程序的)都存在的时候,我们就可以用子程序的 inheritThreadLocals 来接受 父程序的inheritThreadLocals,这样就实现了父子程序之间的值的传递,举个例子:
1 private void test() { 2 final ThreadLocal threadLocal = new InheritableThreadLocal(); 3 threadLocal.set("帅得一匹"); 4 Thread t = new Thread() { 5 @Override 6 public void run() { 7 super.run(); 8 Log.i( "张三帅么 =" + threadLocal.get()); 9 } 10 }; 11 t.start(); 12 }
我们在主程序(父线程)里创建的 InheritableThreadLocal 在子线程是可以正常访问的。
2、如何理解 ThreadLocal 内存泄漏的问题
我们先看一下 Map 里的引用是怎么定义的:
1 static class Entry extends WeakReference<ThreadLocal<?>> { 2 /** The value associated with this ThreadLocal. */ 3 Object value; 4 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k);//设置K为弱引用 7 value = v; 8 } 9 }
可以看到这里继承了 WeakReference 这个类,将 K 设置成了弱引用,那什么是弱引用?
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
所以可以得出 Key 是一个弱引用,而 Value 则是一个强引用,当线程本身消亡时,这两个引用当然会被解除,不会造成内存泄漏
但如果类似线程池里面,线程得到了复用,而 Key 值在下一次GC就会被消灭,Value值因为线程得到了复用始终没有得到消除,就产生了 key 值为 NULL,而Value值占用了空间的情况,此时就造成了内存的泄漏
那如何防止内存泄漏?
其实之所将 Key 值设为弱引用就是想利用这个特征来防止内存泄漏,当弱引用的 key 被GC回收后,在 ThreadLocal 调用 get() 或 set() 方法后会自动清除 key 值为null的value值。
此外也可以通过调用 remove() 方法来清楚这个值