ThreadLocal是什么?
ThreadLocal提供线程本地变量,每个线程拥有本地变量的副本,各个线程之间的变量互不干扰。ThreadLocal实现在多线程环境下去保证变量的安全。以下来源于ThreadLocal类的注释。
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.
ThreadLocal的使用
ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。
示例1:get和set方法的使用
1 public class ThreadLocalTest { 2 public static void main(String[] args) { 3 ThreadLocal<String> tl = new ThreadLocal<>(); 4 5 new Thread(){ 6 @Override 7 public void run() { 8 try { 9 Thread.sleep(100); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 tl.set("ABC"); 14 // 当前线程共享变量为ABC,Thread-1 Thread-0 ABC 15 System.out.println(Thread.currentThread().getName() + " " + tl.get()); 16 } 17 }.start(); 18 19 new Thread(){ 20 @Override 21 public void run() { 22 try { 23 Thread.sleep(1000); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 // 当前线程共享变量为空,Thread-1 null 28 System.out.println(Thread.currentThread().getName() + " " + tl.get()); 29 } 30 }.start(); 31 } 32 }
运行结果
示例1:remove和initialValue方法的使用
1 public class ThreadLocalTest2 { 2 public static void main(String[] args) { 3 ThreadLocal<String> tl = new ThreadLocal<String>(){ 4 @Override 5 protected String initialValue() { 6 return Thread.currentThread().getName(); 7 } 8 }; 9 10 new Thread(){ 11 @Override 12 public void run() { 13 try { 14 Thread.sleep(100); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 tl.set("ABC"); 19 System.out.println(Thread.currentThread().getName() + " " + tl.get()); 20 tl.remove(); 21 System.out.println(Thread.currentThread().getName() + " " + tl.get()); 22 } 23 }.start(); 24 25 new Thread(){ 26 @Override 27 public void run() { 28 try { 29 Thread.sleep(1000); 30 } catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 System.out.println(Thread.currentThread().getName() + " " + tl.get()); 34 } 35 }.start(); 36 } 37 38 }
运行结果
ThreadLocal的数据结构
Thread类中有个变量threadLocals,这个类型为ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,就是一个普通的Java类,但是实现的类似map的功能。
每个线程都要自己的一个map,map是一个数组的数据结构存储数据,每个元素是一个Entry,entry的key是threadlocal的引用,也就是当前变量的副本,value就是set的值。
ThreadLocal的源码分析
ThreadLocal原理图
1、Thread类中有个变量threadLocals,类型为ThreadLocal.ThreadLocalMap,这个就是保存每个线程的私有数据。
1 public class Thread implements Runnable { 2 3 /* ThreadLocal values pertaining to this thread. This map is maintained 4 * by the ThreadLocal class. */ 5 ThreadLocal.ThreadLocalMap threadLocals = null; 6 7 }
2、ThreadLocalMap是ThreadLocal的内部类,每个数据用Entry保存,其中的Entry继承与WeakReference,用一个键值对存储,键为ThreadLocal的引用。为什么是WeakReference呢?如果是强引用,即使把ThreadLocal设置为null,GC也不会回收,因为ThreadLocalMap对它有强引用。
1 static class ThreadLocalMap { 2 3 /** 4 * The entries in this hash map extend WeakReference, using 5 * its main ref field as the key (which is always a 6 * ThreadLocal object). Note that null keys (i.e. entry.get() 7 * == null) mean that the key is no longer referenced, so the 8 * entry can be expunged from table. Such entries are referred to 9 * as "stale entries" in the code that follows. 10 */ 11 static class Entry extends WeakReference<ThreadLocal<?>> { 12 /** The value associated with this ThreadLocal. */ 13 Object value; 14 15 Entry(ThreadLocal<?> k, Object v) { 16 super(k); 17 value = v; 18 } 19 } 20 21 }
3、ThreadLocal中的set方法的实现逻辑,先获取当前线程,取出当前线程的ThreadLocalMap,如果不存在就会创建一个ThreadLocalMap,如果存在就会把当前的threadlocal的引用作为键,传入的参数作为值存入map中。
1 public void set(T value) { 2 // 获取当前线程 3 Thread t = Thread.currentThread(); 4 // 从当前线程中获取map 5 ThreadLocalMap map = getMap(t); 6 if (map != null) 7 map.set(this, value); 8 else 9 createMap(t, value); 10 }
4、ThreadLocal中get方法的实现逻辑,获取当前线程,取出当前线程的ThreadLocalMap,用当前的threadlocak作为key在ThreadLocalMap查找,如果存在不为空的Entry,就返回Entry中的value,否则就会执行初始化并返回默认的值。
1 public T get() { 2 // 获取当前线程 3 Thread t = Thread.currentThread(); 4 // 从当前线程中获取map 5 ThreadLocalMap map = getMap(t); 6 if (map != null) { 7 // 根据this->threadlocal获取值 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 }
5、ThreadLocal中remove方法的实现逻辑,还是先获取当前线程的ThreadLocalMap变量,如果存在就调用ThreadLocalMap的remove方法。ThreadLocalMap的存储就是数组的实行,因此需要确定元素的位置,找到Entry,把entry的键值对都设为null,最后也Entry也设置为null。其实这其中会有哈希冲突,具体见下文。
1 public void remove() { 2 // 从当前线程中获取map 3 ThreadLocalMap m = getMap(Thread.currentThread()); 4 if (m != null) 5 m.remove(this); 6 } 7 8 9 private void remove(ThreadLocal<?> key) { 10 Entry[] tab = table; 11 int len = tab.length; 12 int i = key.threadLocalHashCode & (len-1); 13 for (Entry e = tab[i]; 14 e != null; 15 e = tab[i = nextIndex(i, len)]) { 16 if (e.get() == key) { 17 e.clear(); 18 expungeStaleEntry(i); 19 return; 20 } 21 } 22 }
解决哈希冲突
ThreadLocal中的hash code非常简单,就是调用AtomicInteger的getAndAdd方法,参数是个固定值0x61c88647。
上面说过ThreadLocalMap的结构非常简单只用一个数组存储,并没有链表结构,当出现Hash冲突时采用线性查找的方式,所谓线性查找,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。如果产生多次hash冲突,处理起来就没有HashMap的效率高,为了避免哈希冲突,使用尽量少的threadlocal变量
ThreadLocal为什么要使用弱引用和内存泄露问题
1、在ThreadLocal的生命周期中,都存在这些引用。看上图: 实线代表强引用,虚线代表弱引用。
2、ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap
映射表,这个映射表的 key 是 ThreadLocal
实例本身,value 是真正需要存储的 Object。
3、也就是说 ThreadLocal
本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap
获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap
是使用 ThreadLocal
的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
4、ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
5、总的来说就是,ThreadLocal里面使用了一个存在弱引用的map, map的类型是ThreadLocal.ThreadLocalMap.
Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。
但是,我们的value却不能回收,而这块value永远不会被访问到了,所以存在着内存泄露。因为存在一条从current thread
连接过来的强引用。只有当前thread结束以后,current thread
就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是将调用threadlocal的remove方法,这也是等会后边要说的。
6、其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()
的时候都会清除线程ThreadLocalMap里所有key为null的value。
所以,ThreadLocal的建议使用方法:
-
设计为static的,被class对象给强引用,线程存活期间就不会被回收,也不用remove,完全不用担心内存泄漏
-
设计为非static的,长对象(比如被spring管理的对象)的内部,也不会被回收
-
没必要在方法中创建ThreadLocal对象