ThreadLocal底层源码解析

线程隔离,保证多线性访问安全

每个线程拿到的值私有,相互不干扰
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题

线程内的资源共享

1.多线程并发场景
2.通过ThreadLocal在同一个线程,不同组件中传递公共变量
3.每个线程变量独立,不会相互影响

使用场景

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。一个线程用自己的connection

基本方法

ThreadLoacl()创建对象
set设置当前线程绑定的局部变量
get得到当前线程绑定的局部变量
remove移除当前线程绑定的局部变量
在这里插入图片描述
有人说可以用synchronized,用这个效率低。只能一个个执行,会造成性能下降,并发下降

ThreadLocal和synchronized区别

synchronized时间换空间,ThreadLocal是空间换时间,每个线程提供变量副本,自己用自己的,每个线程相互隔离。

ThreadLocal中都会存在ThreadLoaclMap集合。集合里面存储的是key,value形式的数据,key代表的是threadlocal的一个成员对象,value对应的是当前线程里面的一个数据。
ThreadLocal是弱引用,允许被回收

ThreadLocal内部设计

在这里插入图片描述
这样设计好处是

  1. 每个Map存储的Entry数量变少(Thread线程数量多于ThreadLocal),避免hash冲突
  2. Thread销毁时,ThreadLocalMap也会销毁,减少内存使用。(早期线程执行结束不会把ThreadLocal带走)

每个Thread都有个ThreadLocalMap,map的key是ThreadLocal本身,value才是真正存储的值object。即Map里面存储的是ThreadLocal对象和变量副本value

基于内部结构看源码

set方法

public void set(T value) {
		//获取当前线程对象
        Thread t = Thread.currentThread();
        //获取此线程对象中的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //判断map是否存在
        if (map != null) {
        //存在set此实体entry
            map.set(this, value);
        } else {
        //当前线程不存在ThreadLocalMap,开始初始化map对象,将当前线程和value作为第一个entry放到map中
            createMap(t, value);
        }
    }
    //初始化
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    //初始化
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//16
            table = new Entry[INITIAL_CAPACITY];
            //通过线程计算下标值
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //new Entry,放入线程thread】和value
            table[i] = new Entry(firstKey, firstValue);
            //size修改为1,第一次初始化大小1
            size = 1;
            //threshold赋值,threshold = 16 * 2 / 3;初始容量的三分之二
            setThreshold(INITIAL_CAPACITY);
        }

获取当前线程维护的ThreadLocalMap

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

在这里插入图片描述

get方法

首先获取当前线程,根据当前线程获取一个Map
如果获取的Map不为空,则在map中以ThreadLocal的引用作为key来在map中获取对应的entry,
如果entry不为空,返回value
否则map为空或者entry为空,通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为key和value创建一个新的Map

public T get() {
        Thread t = Thread.currentThread();
        //获取当前t线程的map
        ThreadLocalMap map = getMap(t);
        //不为null
        if (map != null) {
        	//以当前ThreadLocal为key,得到entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取value
                T result = (T)e.value;
                return result;
            }
        }
        //初始化,map不存在,表示此线程没有维护的map对象,
        //map存在,但是没有当前ThreadLocal关联的entry
        return setInitialValue();
    }
    
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);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

remove方法,获取map,移除

protected T initialValue() {
        return null;
    }

ThreadLocalMap源码分析

map里面有Entry,ThreadLoaclMap是Threadlocal的内部类。Entry这个键值对也是独立实现的
Entry继承WeakReference,弱引用

static class ThreadLocalMap {
		//继承弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
			//key是ThreadLocal
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        }

在这里插入图片描述

弱引用和内存泄露

java四种引用,强软弱虚。
弱引用,垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收
内存泄露,指程序已经动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存浪费,导致程序运行减慢甚至崩溃,最终内存溢出。

如果key为null(弱引用,会回收)。
突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

在这里插入图片描述
entry不回收
在这里插入图片描述
entry中key是null,但是没有手动删除entry,当前线程还在运行,虽然key是null,但是value还有值,value不会用到,不会回收,导致内存泄露
在这里插入图片描述
解决办法:使用完ThreadLocal后,执行remove操作,删除entry,避免出现内存溢出情况。

内存泄露的根源,和强弱引用无关,是没有手动删除entry导致内存泄露。

那为什么源码要使用弱引用

强弱都有可能有问题
在这里插入图片描述
ThreadLocalMap中set方法,getEntry方法,会对key=null进行判断,如果是null(ThreadLocal弱引用变成null),会对value也置为null。多了层保障。就算忘了remove,弱引用比强引用多了这层保障。
只是多一层保障,彻底避免还是要使用完后remove

Hash冲突

初始化map时候

//通过线程计算下标值
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

在这里插入图片描述
这个特殊的hash值和斐波那契数列有关,主要目的就是为了让hash码能均匀分布在2的n次方的数组里,也就是Entry[]中,这样做可以尽量避免hash冲突

hashcode&(size-1),hash发生冲突次数减少

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.

            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;
                }
				//如果key是空,但是value不是,说明已经被回收,当前是个旧的无用的
                if (k == null) {
                //用新元素替换旧元素,垃圾清理,防止内存泄露
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            //cleanSomeSlots清除null元素
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
            //超过阈值,再hash
                rehash();
        }

扩容

在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:

private void rehash() {
            expungeStaleEntries();
通过判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4 来决定是否扩容
            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

这里首先是会进行探测式清理工作,从table的起始位置往后清理。清理完成之后,table中可能有一些key为null的Entry数据被清理掉,所以此时通过判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4 来决定是否扩容。我们还记得上面进行rehash()的阈值是size >= threshold,所以当面试官套路我们ThreadLocalMap扩容机制的时候 我们一定要说清楚这两个步骤

这里有扩容代码
扩容后的tab的大小为oldLen * 2,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entry为null的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中了。重新计算tab下次扩容的阈值。

private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

在这里插入图片描述

如何正确的使用ThreadLocal

1、将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露

2、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

InheritableThreadLocal

使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。
继承ThreadLocal并重写方法
childValue/getMap/createMap
可以实现 子线程中使用父线程中的线程本地变量

为什么可以在子线程中传递?
在Thread类中除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。

Thread类的部分代码如下:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

最关键的一点是,在它的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。

在线程池中如何共享ThreadLocal对象生成的数据?

因为涉及到不同的线程,如果直接使用ThreadLocal,显然是不合适的。

TransmittableThreadLocal

使用TransmittableThreadLocal,它并非JDK自带的类,而是阿里巴巴开源jar包中的类。

可以通过如下pom文件引入该jar包:

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>transmittable-thread-local</artifactId>
   <version>2.11.0</version>
   <scope>compile</scope>
</dependency

使用TtlExecutors.getTtlExecutorService方法,去创建ExecutorService对象。

这是非常重要的地方,如果没有这一步,TransmittableThreadLocal在线程池中共享数据将不会起作用。

使用场景

1.框架中,开发者在一个线程中设置的数据需要在后续的子线程中继续使用
2.线程池中线程被多个任务复用,为了保证每个任务所使用的数据隔离

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值