平常的开发过程中,如果有个类不是线程安全的,比如SimpleDateFormat,要使这个类在并发的过程中是线程安全的,那么可以将变量设置位局部变量,不过存在的问题就是频繁的创建对象,对性能和资源会有一定降低和消耗;那么这里就可以用到ThreadLocal作为线程隔离,那么ThreadLocal是如何实现线程与线程之间隔离的呢,待会儿会在下文进行讲解。
之前有篇使用ThreadLocal做线程之间的隔离实例,大家可以参考一下:使用ThreadLocal实现Mybatis多数据源
JDK引用类型
在了解ThreadLocal原理之前,先让大家了解一下JDK中的引用类型。
JDK共有四种引用类型:
1、强引用类型:就是平常创建的对象都属于强引用类型,比如 Object object = new Object();该object为强引用类型,如果该引用没有主动置为null,那么该引用的对象就不会被GC回收,所以一般在写完一段业务之后都会将用到的对象引用置为null,就是为了辅助GC更好的进行垃圾回收。
2、软引用类型:比强引用的类型弱一点,在应用程序发生OOM(内存溢出)之前就会去回收这些弱引用占用的内存,使用的SoftReference类,使用示例如下:
3、弱引用类型:比软引用类型还要弱一点,在下一次发生GC回收之前就会被垃圾回收器进行回收,使用WeakReference类,使用示例如下:
4、虚引用类型:这个是JDK中最弱的引用类型,在对象被回收之前会移入到一个队列当中,然后在进行删除,这个引用类型用的不多,在JDK中引用的类是PhantomReference,这个需结合引用队列(ReferenceQueue)以及重写finalize方法进行使用,使用示例如下:
对于应用场景来说,如果当前对象为可有可无的话,那么可以使用软引用或者弱引用进行使用,而对应虚引用的话,个人认为可以适用在非GC回收的区域(比如:元空间MetaSpace)使用,可以用来监测这些区域的回收情况等。
基本原理
ThreadLocal的用法呢,这里就先不谈;底层使用的是ThreadLocalMap这个Map的底层采用的是ThreadLocalMap.Entry,ThreadLocalMap这个类在每个线程当中都会存在一份对应的单独得对象,在Thread类中变量如下:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
这里就是线程安全的重点,因为并发的情况下,每个线程都对应着有份自己的ThreadLocalMap,所以就不存在多线程竞争资源问题,所以如果使用ThreadLocal建议和线程一起使用,以为这样可以减少系统的性能开销以及对ThreadLocal对象的一种复用,提升系统性能。
看一下ThreadLocalMap,会发现Entry继承了WeakReference类,说明这个类创建出来的对象是弱引用对象
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
//容量
private static final int INITIAL_CAPACITY = 16;
//存储属性值
private Entry[] table;
//构造初始化table数组
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}
我们开看一下get方法:获取当前线程,然后通过调用getMap方法获取到当前线程的ThreadLocalMap对象,对于刚开始初始化的线程或者在ThreadLocalMap中没有找到来说,那么就会走下面的setInitialValue方法,如果已经初始化的ThreadLocalMap来说,会直接获取对应的Entry对象
/*** Returns the value in the current thread's copy of this* thread-local variable. If the variable has no value for the* current thread, it is first initialized to the value returned* by an invocation of the {@link #initialValue} method.** @return the current thread's value of this thread-local*/public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}
在setInitialValue方法中,会发现去获取初始化方法中的对象,如果在创建ThreadLocal没有重写初始化方法的话,那么就会返回null,或者在使用前调用set方法重新设置一下当前线程中的ThreadLocalMap中的属性初始化值,大家可以各自去看一下set方法;
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}
这里列一下getEntry方法,根据ThreadLocal对象计算出hash值找到对应的table数组的位置,并且获取到这个对象。
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);
}
问题
以为这就完了?并没有,规范的使用为在使用过get方法应该在调用remove方法进行删除(即将当前线程ThreadLocalMap中的引用置为空并且table数组中的Entry值也置为空);如果是线程池的话,那么这些数据就会一直存在,如果没有及时删除会造成内存泄漏。
调用remove方法回去执行ThreadLocalMap的remove方法,而在这个方法中,通过计算得到对应的Entry数组的位置,并且进行引用清除以及table数组清空,避免内存泄漏问题,
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
/*** Remove the entry for key.*/
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {//将Entry本身的引用ThreadLocal置为空e.clear();//清空table数组中Entry.valueexpungeStaleEntry(i);return;}}
}
但是如果是经常性用到的ThreadLocal的话,个人建议可以不用删除,因为如果频繁使用的话,置为null,后面又会重新调用在ThreadLocalMap未找到,那么就会调用setInitialValue方法,重新创建对象并且赋值,在某种意义上可以说是和局部变量是一样的了,这样就违背了当初减少性能开销的需求了。
在加上Entry继承了WeakReference类,所以创建的对象会是个弱引用类型,在GC进行回收时候会被回收掉的,如果回收掉了引用对象,那么Entry中的value变量值是否还存在呢;
继续解析
注意这里是回收掉引用的对应,即ref.get()为null值,但是弱引用本身这个对象是还在的,我们看一下在setInitialValue方法中是如何处理的,重新设置里面,如果获取到ThreadLocal引用没有获取到,说明这个弱引用被回收了,这里就会去调用replaceStaleEntry方法。
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);//重新设置值elsecreateMap(t, value);return value;
}
//ThreadLocalMapprivate void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}
而在replaceStaleEntry方法中有个这么一行代码,将value变量置为null并且重新创建Entry对象,所以就算是没有调用remove删除方法,在GC过后依旧会置为null。
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
在此会有个小问题,为啥不用SoftReference而是使用WeakReference,个人觉得如果使用软引用的话,如果是使用线程池并且ThreadLocal会频繁访问的话,那么是可以的,但是实际应用并非只有这种情况,而且在发生OOM之前,只会回收掉软引用对象,但是Entry中的value变量还在,并不能真正的回收掉值,只有等到下一次使用的时候才能置为null,所以综合来看使用WeakReference还是最好的选择。
欢迎各位大佬一起讨论