Java多线程之ThreadLocal

前言

在前面几章Java多线程相关的文章中,相信大家已经对多线程有了一定的基本了解,那么我们今天看一个平时工作中可能不太常用或者没有接触到过的一个工具类ThreadLocal。在前面volatie相关的文章中我有提到过JMM内存模型,那么ThreadLocal其实就是在每个线程内部给我们维护一个变量的副本,这时候我们线程操作的变量就都是每个线程内部独有的副本变量,从而达到线程安全的一个效果。但是ThreadLocal还有另一个作用:隐式传参。接下来我们先了解一下ThreadLocal的用法吧。

ThreadLocal的用法

在这里插入图片描述
我们可以通过输出的结果发现,在主线程输出的为null 而再我们其他线程内部就会输出69这个数值。
所以我们再使用之前需要现在当前线程中给ThreadLocal的set方法给定一个对象,他么当前线程的其他方法都可以用过ThreadLocal.get方法获取到这个对象也就印证了我们刚刚的说法

  1. 线程安全
  2. 隐式传参

ThreadLocal实现原理

那么ThreadLocal到底是怎么实现出来的呢?答案就在我们的Thread类。我们看一下Thread类源码

Thread类内部结构

在这里插入图片描述
我们发现,在Thread类内部成员变量会有两个属性,他们都是ThreadLocal当中的一个ThreadLocalMap这个类。而ThreadLocalMap的数据结构其实也就是我们经常使用的HashMap的KV形式。而ThreadLocalMap中K就是我们当前的线程,而V就是我们通过ThreadLocal.set方法穿进去的对象值。而当我们每次调用theadlocal.get方法的时候我们是不是就可以通过当前线程从而找到对应的对象。事实上ThreadLocalMap中getMap也是这么做的,我们看一下
在这里插入图片描述

ThreadLocal类

我们刚刚知道了通过theadlocal.set方法给当前线程副本设置一个副本对象,threadlocal.get方法可以获取到这个副本对象,那么他们是怎么实现的呢?

set方法

public void set(T value) {
		//获取当先线程信息
        Thread t = Thread.currentThread();
        //通过getMap方法获取到线程内部的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //判断如果map不为空则给map赋值
        if (map != null)
            map.set(this, value);
        else
        //给当先线程内部的ThreadLocalMap创建一个对象并通过ThreadLocalMap构造方法设置一个初始值
            createMap(t, value);
    }

get方法

    public T get() {
    	//获取当前线程信息
        Thread t = Thread.currentThread();
        //获取线程内部ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//通过threadlocal的hashcode计算出索引下标,拿到entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //返回entry对象中的value对象
                T result = (T)e.value;
                return result;
            }
        }
        // 如果不存在则设置一个初始值为null的值并返回
        return setInitialValue();
    }

通过上面两个方法我们发现,其实ThreadLocal一直操作的都是ThreadLocalMap对象,所以ThreadLocal的重点在于TheadLocalMap上面,而并非ThreadLocal自身。

ThreadLocalMap

既然ThreadLocalMap是一个k,v形式存储的数据结构,并且他会通过计算hashCode获取索引下标,所以它其实跟我们的hashMap差不多,使用hash表存储数据,使用链表解决hash冲突。那么既然有链表就肯定有每一个用来存储和链接起来的节点。我们看一下他的entry。
在这里插入图片描述
大体上来说和hashMap差不多,但是有一个很大的区别就是ThreadLocalMap中的Entry节点继承了weakReference。那么这个是什么呢?这个就需要从我们对象的引用类型来讲解了。

首先再我们java中对象的引用类型可以分为四种

  1. 强引用
  2. 弱引用
  3. 软引用
  4. 虚引用

而这四种引用类型对应不同的垃圾回收策略。
1.强引用:被引用的对象都会在我们的GC ROOT引用链上存在,而当引用链断开,则该对象被回收。而当内存不足的时候则会抛出OOM异常
2.弱引用:不管当前对象是否存在于GC ROOT上,也不管内存空间到底足不足,只要触发了垃圾回收机制,则会将当前对象回收
3.软引用:当我们的内存空间不足时候才会回收该对象,否则将不会进行回收
4.虚引用:虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

而ThreadLocalMap中Entry继承的weakReference代表的他是一个弱引用,而我们的Entry中实际上作为K的是ThreadLocal而value则是我们给定的对象。那么我们想一下,当我们GC的时候虽然我们的Entry节点一直被我们的链表引用这,这时候虽然存在于GC ROOT引用链上,但是由于我们的Key是一个弱引用,那么此时GC就会回收掉我们的Key,但是value还是一直保持着引用,那么就会出现一个问题。此时这个entry节点上的value我们已经无法定位到它了,因为我们的key已经被回收了,所以这时候会有一个很严重的问题:内存泄漏
内存泄漏和内存溢出他们是两种概念,第一种是因为内存无法被释放导致频繁FULL GC甚至内存溢出。而内存溢出很简单,其实就是内存不足但是我们还在继续往内存添加对象导致内存没有办法再支撑下去,从而抛出OutOfMemery异常。

所以在使用完ThreadLocal之后我们需要自己手动的清除掉ThreadLocal。

ThreadLocalMap构造方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化一个hash表,容量为16
    table = new Entry[INITIAL_CAPACITY];
    //第一个key的hash值会设置为HASH_INCREMENT = 0x61c88647,通过
    //这个值去计算一个索引下标,每进来一个线程,线程的hashcode会增加
    //0x61c88647
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //ThreadLocalMap的扩容阈值就是table的大小
    setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap中的Set方法

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.
    //通过hash表容量和key的hash之计算出一个索引下标
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //遍历链表存放数据到ThreadLocalMap中
    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;
        }
    }
    //当没有找到对应的线程的ThreadLocal时候,则在对应的ThreadLocalMap的table
    //初始化一个entry节点,存放ThreadLocal副本和值
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //执行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

总结

那么我在这里对ThreadLocal的set方法自己做了个总结,大家也可以自己将剩下的一部分总结一下哦
在这里插入图片描述
文章这里就结束了,喜欢的话记得点赞收藏评论转发

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值