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的主要内容,如果未能解决你的问题,请参考以下文章

线程的补充

ThreadLocal

ThreadLocal

多线程之ThreadLocal

JDBC: ThreadLocal 类

关于ThreadLocal