threadlocal get为空_ThreadLocal源码学习、模拟一个ThreadLocal

ThreadLocal 类的文章网上很多,自然有讲解不同知识点的,我们这里从弱引用会被GC回收吗、环形队列的使用以及静态内部类三个知识点来学习ThreadLocal 类。具体涉及到的点如下。

a、Thread、ThreadLocal、ThreadLocalMap、存储值Value 之间的关系

b、ThreadLocal 中存储的变量在GC之后会被回收吗

c、一个线程中创建多个ThreadLocal 存储值和多个线程向一个ThreadLocal 对象中存储值的关系

d、ThreadLocal 内部为何使用环形队列

e、ThreadLocal 是怎么实现变量的线程隔离的

f、静态内部类在ThreadLocal、ThreadLocalMap 中的使用

我们带着这些疑问开始ThreadLocal 的学习……

Thread、ThreadLocal、ThreadLocalMap、Entry、存储值value之间的关系

一、多个线程使用同一个ThreadLocal 对象

当多个线程使用同一个TreadLocal对象来设置线程私有变量的时候他是怎么做到线程隔离的?

13db4fef7ac8e9e3ac04c72ecf4b183c.png

看着图我们来梳理一下通过调用treadLocal的set方法在线程中绑定一个线程私有变量的过程。

1、创建一个ThreadLocal 对象,引用变量threadLocal引用改对象

2、调用threadLocal.set(value) 方法

3、获取运行当前方法的线程对象即Thread对象,引用变量为thread1

4、判断thread1对象的ThreadLocal.ThreadLocalMap引用类型所引用对象是否为空,如果为空则创建一个ThreadLocal.ThreadLocalMap对象

5、创建ThreadLocalMap对象的时候会在ThreadLocalMap构造函数中会创建一个Entry 数组

6、将第4步创建的ThreadLocal.ThreadLocalMap对象赋值给线程thread1 中的reference 引用变量即一个线程绑定一个ThreadLocalMap 对象。

7、调用ThreadLocalMap的set(ThreadLocal key,Object value) 方法

8、方法中创建Entry对象,按照上图可以看到Entry对象中有两个属性即ThreadLocal的弱引用 weakReference  和Object 的value ,按照上图可以看到这里对ThreadLocal对象的引用是弱引用。弱引用的实现方式以及特性之后在聊

9、将新创建的Entry 对象保存到ThreadLocal.ThreadLocalMap对象的Entry[] table 数组中。index索引位置就是ThreadLocal 对象的threadLocalHashCode属性%(Entry[]数组长度)

总结:

这样一个线程绑定私有变量过程就结束了,如果Thread2线程中调用threadLocal的set(value)方法进行设置也是上面的这样一个流程。

1、通过图形、流程都能发现ThreadLocalMap不是共享的,每个线程都有自己的ThreadLocalMap实例(threadLocals/inheritableThreadLocals)。

2、threadLocalMap实际上维护的是ThreadLocal实例和set的值之间的映射关系。

3、所以说,ThreadLocal的线程安全正是因为不同线程有自己的ThreadLocalMap即隔离而非共享。

二、一个线程中创建多个ThreadLocal对象

上面是多个线程使用同一ThreadLocal对象,然后调用set方法来设置绑定线程私有变量,下面来看看一个线程中创建多个ThreadLocal对象设置值的过程。

bc2a3b61c190a6b94516bd13ad7efe6d.png

1、创建一个ThreadLocal 对象,引用变量threadLocal引用改对象

2、调用threadLocal.set(value) 方法

3、获取运行当前方法的线程对象即Thread对象,引用变量为thread1

4、判断thread1对象的ThreadLocal.ThreadLocalMap引用类型所引用对象是否为空,如果为空则创建一个ThreadLocal.ThreadLocalMap对象

5、创建ThreadLocalMap对象的时候会在ThreadLocalMap构造函数中会创建一个Entry 数组

6、将第4步创建的ThreadLocal.ThreadLocalMap对象赋值给线程thread1 中的reference 引用变量即一个线程绑定一个ThreadLocalMap 对象。

7、调用ThreadLocalMap的set(ThreadLocal key,Object value) 方法

8、方法中创建Entry对象,按照上图可以看到Entry对象中有两个属性即ThreadLocal的弱引用 weakReference  和Object 的value ,按照上图可以看到这里对ThreadLocal对象的引用是弱引用。弱引用的实现方式以及特性之后在聊

9、将新创建的Entry 对象保存到ThreadLocal.ThreadLocalMap对象的Entry[] table 数组中。index索引位置就是ThreadLocal 对象的threadLocalHashCode属性%(Entry[]数组长度)

10、在同一个线程中创建多个ThreadLocal并调用set(value)方法的时候,1-9是一样的,所以不同ThreadLocal对象以及和其映射的对象value 都会被组装为Entry对象存储在ThreadLocalMap对象的Entry[] 数组中。

11、1-9的整个过程是一样的,但第九步就不同了因为不同ThreadLocal对象的threadLocalHashCode 值有可能一样,有可能不一样,所以计算的index值可能一样可能不一样,如果不一样那就直接按照index存储,如果一样这里用到了线性探测来寻找null位置来存储。

总结:

1、通过上面的学习我们能看到Thread、ThreadLocal、ThreadLocalMap、Entry、存储值value之间的关系。ThreadLocalMap对象的Entry数组中的Entry对象存储了ThreadLocal和value的对应关系。

2、在整个过程中ThreadLocal 对外提供了方法暴露,通过ThreadLocal的方法给当前线程绑定了一个ThreadLocal.ThreadLocalMap 对象。

3、每一个线程都会有唯一一个ThreadLocal.ThreadLocalMap 对象,所以线程间的ThreadLocal.ThreadLocalMap 不是共享的是线程私有的。

OK 这样看明白了他们之间的关系下面来看看经常会问题的弱引用问题……

ThreadLocalMap.Entry类以及他的属性

一、首先我们来看看弱引用会被GC回收吗、弱引用的引用关系是怎样的?

首先来看一段测试代码

1、User实例

static class User {    int age;    String name;    User(int age, String name) {        this.age = age;        this.name = name;    }    public String getName() {        return name;    }    public int getAge() {        return age;    }}

2、测试类

public static void testWeakReference() {        //创建对象,并且被强引用持有        User user = new User(18, "www");        //该对象又被弱引用持有        WeakReference weakUser = new WeakReference<>(user);        //断开强引用        user = null;        //通过弱引用,打印对象        System.out.println("GC前:" + JSON.toJSONString(weakUser.get()));        System.out.println("GC");        System.gc();        //通过弱引用,打印对象        System.out.println("GC后:" + JSON.toJSONString(weakUser.get()));    }

3、测试结果

GC前:{"age":18,"name":"www"}GCGC后:null

再来看看他们的引用关系

fa38a1ae982a30e90cfdec5d2006d048.png

看上面代码,在对比图发现User对象被两个引用变量引用,如果User类型引用变量的强引用断开则只有WeakReference 对象的referent 弱引用User对象,看上面代码的执行结果发现被弱引用变量引用的对象在GC时会被回收。

二、再次看ThreadLocalMap中Entry 数组中的Entry对象对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;    }}

当我们创建一个Entry对象的时候第一个参数ThreadLocal 对象是被WeakReference 弱引用的,第二个值value是被强引用的,具体关系我们可以再次看这张图。

13db4fef7ac8e9e3ac04c72ecf4b183c.png

这张图中我们看ThreadLocal 对象,对这个对象的引用和上面定义的User的引用关系是一样的,有一个定义是的强引用,也有一个Entry对象中的弱引用,如果定义时的强引用被断开则只有Entry对象中的弱引用,如果这时发生GC 则ThreadLocal 对象是会被回收。

结论:

1、如果一个对象只有弱引用则GC时会被回收

2、如果ThreadLocal 定义时的强引用被断开则GC的时候会把Entry对象中被弱引用引用的ThreadLocal对象会被回收这时WeakReference.get()返回的就是null

那项目中使用ThreadLocal 来存储线程私有数据会丢失吗?

1、如果定义ThreadLocal时的强引用还在则不会丢失。

2、如果把定义时的强引用断开则失去了强引用会被GC回收,数组也会被环形队列的算法清除。

3、自己造的轮子 MyThreadLocal 代码 https://github.com/BitterCaffe/inner-klass

ThreadLocalMap.Entry中的key为何使用弱引用?

1、如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

环形队列

1、ThreadLocalMap 中定义了一个Entry[] table 数组,那数组怎么就变成环形队列了那还是看代码

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

通过这两个方法或者说一个方法,数组就能当环形队列使用了(当然实现没这么简单)

2、如果一个线程中定义多个ThreadLocal 并调用方法存储线程私有数据则会出现index重复的情况,调用nextIndex(int index,int len) 方法进行线性探测获取null的位置存储Entry对象,来看看set方法:

private void set(ThreadLocal> key, Object value) {    //ThreadLocalMap中定义的数组    Entry[] tab = table;    int len = tab.length;    //计算index位置    int i = key.threadLocalHashCode & (len-1);    //线性探测index索引位为null的索引下标    for (Entry e = tab[i];         e != null;         e = tab[i = nextIndex(i, len)]) {        ThreadLocal> k = e.get();        //如果相等直接替换        if (k == key) {            e.value = value;            return;        }        //如果没有强引用,只有弱引用并且被GC回收则删除并添加新Entry        if (k == null) {            replaceStaleEntry(key, value, i);            return;        }    }    //index索引位null 则存储Entry对象    tab[i] = new Entry(key, value);    int sz = ++size;    //代码精简}

当然环形队列的实现没这么简单,魔数的选择,线性探测、过期数据清理、清理位置的查找、队列阀值、队列扩容、重新取模存储 等都要实现,如果对环形队列感兴趣的可以看看ThreadLocal 的源码注释+代码700多行代码……

静态内部类

1、静态内部类创建对象不依赖寄宿外部类对象

2、静态内部类不能访问寄宿外部类的非静态属性、静态方法

3、静态内部类中能定义静态变量、非静态变量、静态方法、非静态方法、静态内部类,非静态内部类中不能定义静态变量、静态方法、静态代码块

4、静态内部类的创建不会初始化外部类,只有静态内部类调用了外部类的static方法、变量才会被初始化

5、从JVM运行的角度来分析,每个类都会有一个.class文件然后JVM加载、验证、准备、解析、初始化所以javac编译器在编译的时候会动态生成一个内部类的 .class文件。

6、从JVM的角度来分析,内部类、静态内部类都是外部类的attributes属性中的 InnerClasses属性,但是静态内部类的上面几个特性所以静态内部类对象的创建是不依赖外部对象,因为不能访问外部类对象的信息所以也就不用创建外部类对象,只需要加载外部类获取外部类的元数据就OK。

疑问:静态内部类是怎么关联外部类,获取外部类元数据信息的?

这个疑问希望后期能补充上,那样对内部类的理解就更清晰了……

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值