ThreadLocal
Posted morph
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal相关的知识,希望对你有一定的参考价值。
ThreadLocal的实例代表了一个线程局部的变量,只能在当前线程内被读写,不被其他线程共享。比如有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadLocal变量的引用,但是这两个线程依然不能看到彼此的ThreadLocal变量。
简单的来说,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本,并非共享变量。
举个例子
public class ThreadLocalDemo private static final AtomicInteger count = new AtomicInteger(0); private static final ThreadLocal<Integer> t = ThreadLocal.withInitial(() -> 0); public static void main(String[] args) t.set(0); 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<ThreadLocal<?>> /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) super(k); value = v;
其内部持有一个Entry变量,key是ThreadLocal为弱引用。该对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
那么先分析下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。
ThreadLocalMap的UML类图如下:
内存泄漏
如果在线程池中使用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
以上是关于ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章