Android源码---ThreadLocal

 

关于ThreadLocal平时用的真的不多,目前做了有7,8个项目,都没有使用过ThreadLocal,所以对ThreadLocal还是比较陌生。对它的唯一认识是从Handler的源码中,也知道ThreadLocal是做线程数据隔离的,其他的就一概不知了。最近打算好好看看ThreadLocal源码。发现了好多新的知识,对ThreadLocal也有了更深的认识。而ThreadLocal在多线程中也有着比较重要的地位。这篇博客我会从使用,源码,问题这三个方面进行分析

一、ThreadLocal的使用

我相信不太了解ThreadLocal的同学,至少也知道ThreadLocal可以做线程间数据的隔离,线程之间的数据互不影响。而ThreadLocal对外开放的方法主要就是
我们来看一下它的使用

public class Main2 {
    public static void main(String[] args) {
        ThreadLocal<String>threadLocal = new ThreadLocal<>();
        //存入数据
        threadLocal.set("gzc");
        //取出数据
        System.out.println("result:"+threadLocal.get());
        //移除数据
        threadLocal.remove();
    }
}

有的小伙伴会说,你这个写的是什么呀?也没有体现出来线程间的数据隔离啊!是的,上面只是三个普通的方法的调用。我们继续看下面的代码:

public class Main2 {
    public static void main(String[] args) {
        ThreadLocal<String>threadLocal = new ThreadLocal<>();
        threadLocal.set("gzc");

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("sub_gzc");
                System.out.println(Thread.currentThread().getName()+"  result:"+threadLocal.get());
                threadLocal.remove();
            }
        },"sub_thread").start();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+"  result:"+threadLocal.get());
        threadLocal.remove();
    }
}

首先我们依然在主线程中给ThreadLocal对象进行了赋值,之后开启一个子线程,使用同样的ThreadLocal对象进行了赋值,获取,移除。我们在主线程中延迟了一段时间,保证子线程先执行。

之后再在主线程中打印threadLocal的值。我们看结果

sub_thread  result:sub_gzc
      main  result:gzc

从结果中我们可以看出来,子线程先执行并且子线程中的threadLocal的操作并不会影响主线程中的threadLocal中的值。所以我们平时如果用到ThreadLocal的话,可以把ThreadLocal定义为static,这样就可以全局使用了。而阿里规范中也提到过:

ThreadLocal无法解决共享对象的更新问题,ThreadLocal对象建议使用static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配了一块存储空间,所以此类的对象(只要在这个线程内定义的)都可以操控这个变量。

什么叫做无法解决共享对象的更新问题?每个线程向ThreadLocal中读写数据是线程隔离,互相之间不会影响的。

在使用的时候,为什么要进行remove,其实是为了防止内存溢出的问题,我之后会说这个地方。

如果在一个线程中使用了多个ThreadLocal会怎么样?同学可以自行测试一下,多个ThreadLocal所保存的结果互不影响。如果我们在多线程的情况下使用ThreadLocal,这个情况会出现很多,毕竟,我们要保存的数据不是只有一个,可能是多个。所以会有多个ThreadLocal,这种情况很正常。

关于ThreadLocal的使用,其实也就是这么多了。下面,我们从源码中来看ThreadLocal

二、ThreadLocal的源码

我们如果进入ThreadLocal的源码中,我们可以发现不论是set还是get和ThreadLocal的内部类ThreadLocalMap息息相关,我们以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);
    }

我们可以看到,在进行值的存储时,就是以ThreadLocal为key。这么看来,ThreadLocalMap和我们使用的Map很像。我们进入这个getMap中看看:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

每个Thread类中,都保存着一个ThreadLocalMap

我们进入ThreadLocalMap中,可以看到几个比较关键的地方:

static class ThreadLocalMap {

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

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

        private Entry[] table;

        ......
}

内部类Entry:Entry是key和value的载体,不过Entry是一个弱引用。为什么要使用弱引用呢?请看这篇文章

table:所有的Entry在ThreadLocalMap中都是以数组的形式存储的。

看到这里,估计小伙伴会有很多的问号。为什么要用数组?如果用数组,怎么准确的把Entry取出来?我们一个一个来说清楚

怎么准确的把Entry取出来?

ThreadLocal中有如下几个属性和方法:

    private final int threadLocalHashCode = nextHashCode();


    private static AtomicInteger nextHashCode =
        new AtomicInteger();


    private static final int HASH_INCREMENT = 0x61c88647;


    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

我们可以看见,nextHashCode是AtomicInteger(关于AotmicInteger,小伙伴自行百度~),并且是静态的,所有ThreadLocal实例,共享使用一个nextHashCode。每次ThreadLocal被创建的时候,threadLocalHashCode都会被赋值,而这个值都是通过nextHashCode的getAndAdd方法获取,也就是把这次传入的值和上次的值相加。而这里的值是HASH_INCREMENT,也就是说每次threadLocalHashCode都是上一个threadLocalHashCode加上HASH_INCREMENT。为什么HASH_INCREMENT0x61c88647,建议小伙伴看【JAVA并发编程系列】ThreadLocal 这篇文章,反正我是没懂,但是又感觉有道理....

ThreadLocal在进行值存储的时候,会调用ThreadLocalMap中的set方法,其中有这么一句代码:

int i = key.threadLocalHashCode & (len-1);

这里的i就是table数组的索引,threadLocalHashCode和(len-1)进行了&操作,得出的值一定小于登录len-1,也就是说索引一定在table数组大小范围内。不同的ThreadLocal会不会计算出相同的索引呢?肯定会的。ThreadLocalMap在进行set的时候,先判断此位置是否被占用了或者占用此位置的对象变成了空,如果是,则直接插入;如果不是,得到的索引值就会+1,重新检索添加。取值是一样的道理,如果取到的值不为空并且key相等,那么取出相应的值;否则进行+1重新检索。

为什么要用数组呢?

数组的内存地址可以进行复用,并且是连续的,所以迭代的时候效率更高。因为在ThreadLocalMap中进行了多次清除无用对象的操作,需要进行多次迭代,所以呢使用数组更加合适。

ThreadLocal和ThreadLocalMap还有其他的源码,但是个人认为,上面的代码涉及了一些设计思想,所以更加重要一些。其他的代码更多的是细节的操作,有兴趣的同学可以继续阅读源码或者看参考文章。

三、内存泄露问题

ThreadLocal其实存在着内存泄露的问题,Entry虽然本身是弱引用,但是内部存储的value确是一个强引用。如果ThreaLocal变为了null,JVM扫描到了,就会把Entry回收,但是value这个强引用确回收不了。所以在ThreadLocalMap中有着大量的清除无效Entry的方法。这样做会存在一个问题:Thread的生命周期很长,并不会随时调用清除的方法。依然会存在内存泄露的问题?那我们应该怎么办呢?我们是使用完ThreadLocal之后,调用ThreadLocal中的remove方法进行手动清除即可。贴出一张图,供大家参考:

参考博客

手撕面试题ThreadLocal!!!

【JAVA并发编程系列】ThreadLocal

面试官:知道ThreadLocal嘛?谈谈你对它的理解?(基于jdk1.8)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值