ThreadLocal的实例代表了一个线程局部的变量,只能在当前线程内被读写,不被其他线程共享。比如有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadLocal变量的引用,但是这两个线程依然不能看到彼此的ThreadLocal变量。
简单的来说,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本,并非共享变量。
举个例子
public class ThreadLocalDemo {
private static final AtomicInteger count = new AtomicInteger(0);
private static final ThreadLocal t = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
for(int i = 0; i < 3; i++){
new Thread(() -> {
t.set(t.get() + count.getAndIncrement());
System.out.println(t.get());
}).start();
}
}
}
假设threadlocal是共享的,那么值会是threadlocal和count的值累加的,但实际上打印结果只有count的累加值,这表明每个线程的threadlocal值是局部不共享的。
应用场景
因为ThreadLocal变量,本质上是一种避免共享的方案,由于没有共享,所以自然也就没有并发问题。那么它的适用场景有如下两点:
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中传递,但不希望被多线程共享
第一点,每个线程拥有自己实例,实现它的方式很多,直接在线程内构建实例就可以,但这回。但是ThreadLocal 可以以非常方便的形式满足该需求。
第二点,实例需要在多个方法中传递会导致每个方法都有相同的参数,完全可以将参数提取出来降低耦合度。那么ThreadLocal就是一种很好的实现方式,且更优雅。
实现原理
首先如果让我们自己设计ThreadLocal,根据ThreadLocal的模式:每个线程都有自己的变量,我会想到用map来实现。key是thread,value是每个线程的变量,然后threadlocal持有这个map。
而事实上看源码发现,java的实现方式是thread持有一个map,这个map是ThreadLocal.ThreadLocalMap类型,源码如下:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
其内部持有一个Entry变量,key是ThreadLocal为弱引用,该对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
ThreadLocalMap的UML类图如下:
那么先分析下ThreadLocal的set方法源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
先获取当前线程t,然后获取t的ThreadLocalMap,如果map不为空就将当前threadlocal实例和值set进map中,如果为空就创建一个。
set方法如下:
private 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();
}
简单的说这里运用了开放定址法来解决hash冲突,当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
然后看下ThreadLocal的get方法源码:
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();
}
就是获取当前线程的ThreadLocalMap,通过key也就是当前threadlocal对象获取entry,返回entry中的value。
内存泄漏
如果在线程池中使用ThreadLocal,此时线程的生命周期很长,往往伴随着程序启动和结束,这就意味着ThreadLocal持有的ThreadLocalMap一直不会被回收。
其次ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。
所以为了解决内存泄漏的问题,尽量在使用完变量后调用 remove方法:
ThreadLocal t;
try{
//Todo
}finally{
t.remove();
}
InheritableThreadLocal
通过 ThreadLocal 创建的线程变量,其子线程是无法继承的。也就是说你在线程中通过 ThreadLocal 创建了线程变量 V,而后该线程创建了子线程,你在子线程中是无法通过 ThreadLocal 来访问父线程的线程变量 V 的。
Java 提供了 InheritableThreadLocal 来支持子线程继承父线程的线程变量的特性,InheritableThreadLocal 是 ThreadLocal 子类,所以用法和 ThreadLocal 相同。
但是它具有 ThreadLocal 相同的缺点,可能导致内存泄露。更致命的问题是在线程池中使用InheritableThreadLocal,因为线程池中线程的创建是动态的,很容易导致继承关系错乱,那么很可能导致业务逻辑错误,所以不建议在线程池中使用 InheritableThreadLocal。
总结
总的来说,如果需要在并发场景中使用一个线程不安全的工具类,最简单的方案就是避免共享。避免共享有两种方案,一种方案是将这个工具类作为局部变量使用,另外一种方案就是利用ThreadLocal线程本地存储模式。这两种方案,局部变量方案的缺点是在高并发场景下会频繁创建对象,而使用用ThreadLocal,每个线程只需要创建一个工具类的实例,所以不存在频繁创建对象的问题。
然后源码分析比较浅,有兴趣的话建议参考下这篇文章:
https://www.jianshu.com/p/dde92ec37bd1