ThreadLocal笔记

Posted 做一个苦行僧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal笔记相关的知识,希望对你有一定的参考价值。

 在java中,变量之间共享可以使用public static变量的形式,所有的线程都使用同一个public static变量。如果想实现每一个线程都有自己的共享变量该如何解决呢?这个时候就需要用到ThreadLocal了。类ThreadLocal主要解决的就是每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子可以存储每个线程的私有数据。

先来看一个数据隔离的例子:

public class Tools 
    public static ThreadLocal t1 = new ThreadLocal();


public class Test 
    public static void main(String []args)
        try
            ThreadA threadA = new ThreadA();
            ThreadB threadB = new ThreadB();
            threadA.start();
            threadB.start();
            for (int i=0;i<10;i++)
                Tools.t1.set("Main "+i);
                System.out.println("Main  get value "+Tools.t1.get());
                Thread.sleep(200);
            
        catch (Exception e)

        
    


    static class ThreadA extends Thread
        @Override
        public void run() 

            super.run();
            try
                for (int i=0;i<10;i++)
                    Tools.t1.set("ThreadA"+i);
                    System.out.println("ThreadA get value "+Tools.t1.get());
                    Thread.sleep(200);
                
            catch (Exception e)

            
        
    

    static class ThreadB extends Thread
        @Override
        public void run() 

            super.run();
            try
                for (int i=0;i<10;i++)
                    Tools.t1.set("ThreadB"+i);
                    System.out.println("ThreadB get value "+Tools.t1.get());
                    Thread.sleep(200);
                
            catch (Exception e)

            
        
    

运行上面的代码,打印如下(打印有省略):

从打印的内容可以看出,我们的ThreadLocal类型的Tools.t1 实例,在ThreadA  ThreadB 和main Thread中表现出数据隔离,每个人都有一份,互不干扰

ThreadLocal.java类的代码量不多:

看类接口,也就下面的以下属性和方法,我们这里主要关注的是set(T) ,get()方法。。

public void set(T value) 
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    

ThreadLocalMap getMap(Thread t) 
        return t.threadLocals;
    


//Thread.java类中
ThreadLocal.ThreadLocalMap threadLocals = null;

在调用set方法设置一个值的时候,首先获取当前Thread对象,然后获取ThreadLocalMap对象。在ThreadLocal中其实是 ThreadLocal.ThreadLocalMap在存放值和 取值。调用set方法的时候,map.set(this,value),将当前对象ThreadLocal作为唯一key。但是在每一个线程对象中都存放有一个ThreadLocalMap类型的变量, 后面的createMap就是初始化ThreadLocalMap的

void createMap(Thread t, T firstValue) 
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    

get()方法:

/**
 * 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)
            return (T)e.value;
    
    return setInitialValue();


ThreadLocalMap getMap(Thread t) 
    return t.threadLocals;


private T setInitialValue() 
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;


protected T initialValue() 
    return null;

关于get()方法的解读如下:

1.获取当前线程的ThreadLocalMap对象threadLocals
2.从map中获取线程存储的K-V Entry节点。
3.从Entry节点获取存储的Value副本值返回。
4.map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。

在get方法中,有可能会返回为null ,所以上面第4条也说了,要注意NullPointerException。我们还有另外一种可以预防null的方法,自定义ThreadLocal:

class ThreadLocalExt extends ThreadLocal
        @Override
        protected Object initialValue() 
            return xxxx;
        
    

继承ThreadLocal类,重写initialValue方法,返回默认值,这样在get的时候,如果获取不到值,就不会返回null了。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。

static class Entry extends WeakReference<ThreadLocal<?>> 
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) 
                super(k);
                value = v;
            
        

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置

private static int nextIndex(int i, int len) 
            return ((i + 1 < len) ? i + 1 : 0);
        

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) 
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

ThreadLocalMap的问题

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal<String> threadLocal = new ThreadLocal<Session>();
try 
    threadLocal.set("hello World");
   
 finally 
    threadLocal.remove();

InheritableThreadLocal

    使用InheritableThreadLocal类可以在子线程中取得父线程继承下来的值。可以在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

public static void main(String[] args) 
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        threadLocal.set("帅的一匹");
        Thread t = new Thread(new Runnable() 
            @Override
            public void run() 
                 Log.e("TAG","张三 帅么?  " + threadLocal.get());

            
        );
    

输出  “张三 帅么? 帅的一匹”,我们threadLocal是在main线程中定义的。而我们在子线程Thread中可以访问到。

使用InheritableThreadLocal 不仅可以值继承,而且还可以修改他的值。

 public static void main(String[] args) 
        final ThreadLocal threadLocal = new InheritableThreadLocalExt();
        threadLocal.set("帅的一匹");
        Thread t = new Thread(new Runnable() 
            @Override
            public void run() 
                Log.e("TAG","张三 帅么?  " + threadLocal.get());
            
        );
        t.start();
    


    static class  InheritableThreadLocalExt extends  InheritableThreadLocal
        @Nullable
        @Override
        protected Object initialValue() 
            return "张三帅么?";
        

        @Override
        protected Object childValue(Object parentValue) 
            return parentValue+"  你确定么?";
        
    

自定义InheritableThreadLocalExt 可以重写childValue方法,childValue方法中参数parentValue就是 父线程中的值,也就是,上面代码的最后输出如下:

2020-08-15 22:32:25.674 31927-32194/com.android.recycleview.pingdemo E/TAG: 张三 帅么?  帅的一匹  你确定么?

可以通过childValue修改在子线程中通过InheritableThreadLocal.get()方法的返回值。

以上是关于ThreadLocal笔记的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal学习笔记

ThreadLocal学习笔记

ThreadLocal学习笔记

ThreadLocal学习笔记

ThreadLocal学习笔记

ThreadLocal笔记