ThreadLocal的使用及其原理

ThreadLocal:ThreadLocal是线程局部变量,所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量。

ThreadLocal的使用非常广泛,典型的,mybatis的分页插件PageHelper用的就是ThreadLocal。

在我们日常的开发里,最典型的应用就是例如一个请求(单线程)的执行过程要执行很多方法:a->b->c->d->e,假设方法a要用到一个变量,e也要用到这个变量,如果这个变量一直往下传则会显得很臃肿,这个时候,ThreadLocal是个很好的解决方式

ThreadLocal使用方式

直接看一段代码:

public class ThreadLocalTest {

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        set();
        System.out.println(get()); // 打印 abc
    }

    private static String get() {
        return threadLocal.get();
    }

    private static void set() {
        threadLocal.set("abc");
    }

}

代码很简单,功能也很清晰。下面我们来看看ThreadLocal是如何做到在一个地方set而在另一个地方get的

ThreadLocal的简单分析

1.首先我们来看一下ThreadLocal的初始化

private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

跟进去看一下底层源码:

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public 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);
}

这段代码比较迷糊,好像就是为了初始化一个常量——threadLocalHashCode,等会我们再回来看看作用

2.再来看看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);
    }


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

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

从这里看来,ThreadLocal存放的东西的确是跟当前的线程是有关系的,从字面上来理解,字段是存放在了一个map里,而这个map是当前线程的一个成员变量,这个成员变量的类型是ThreadLocalMap。

下面我们来看看这个ThreadLocalMap是什么东西

我们来看看这个方法

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

看看ThreadLocalMap创建的时候发生了什么事

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }


        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

这里我们第1步的比较迷糊的东西出现了——threadLocalHashCode

看看这段代码做了什么,这段代码的逻辑最终作用是——把该ThreadLocal对应的值存在一个成员变量table里,以key/value的形式存储,key是当前的ThreadLocal实例,value就是我们要保存的值

到这里,我们ThreadLocal存值的原理基本解释完毕了,但是还是有遗留问题

(1)Entry

看看Entry的定义(是ThreadLocalMap一个静态内部类)

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

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

要非常注意,存储ThreadLocal和它的值的是一个弱引用

关于弱引用的意义可以看我这篇文章 强引用、弱引用、软引用和虚引用

所以,当GC发生的时候,定义的ThreadLocal是有可能被回收的

这里讲一下为什么Entry的key(ThreadLocal)要设置成弱引用

ps:这里我们可以看到,这个弱引用的引用者是ThreadLocal,也就是这个Entry的key

我们知道,弱引用有一个特点:当VM在GC的时候,如果这个对象只有弱引用指向,则该对象不管是否存活,都要被回收

那我们的Entry什么情况下会产生这个情况呢?答案就是我们外部的ThreadLocal的生命周期结束了,因为这样就只有软引用定义的引用者(也就是这个ThreadLocal,在这里是Entry的key)在指向这个对象;如果ThreadLocal的生命周期没结束的话,这个ThreadLocal肯定会有强引用在指向的

如果一旦发生ThreadLocal生命周期结束,但是又没有清空Entry(Entry没被清空,Entry的key也就是ThreadLocal还在使用)这种情况,并且又是强引用,会发生什么情况?就会发生如果这个线程不消亡,这个对象就回收不掉的情况,但是这个对象又是可达的(有Entry的key指向),这就产生了 可达但不使用 的情况,就是我们说的内存泄漏

但是如果我们设置成是弱引用,就能尽可能避免这个问题(线程执行完但是Entry没被清,下一次GC的时候,就能把这个对象回收了)

ps:这里的value是不能被定义成弱引用的,因为外部没有强引用指向它,但是key(ThreadLocal)用强引用,这点用ThreadLocal本身作为key的设计还是挺巧妙的

问题到这里,可能又有人有行的问题了——既然Entry里只有key被设置成弱引用,value没有设置,那岂不是value会很容易产生内存泄漏的问题?(因为这时候key被回收了,也就是key变成了null,但是value还是强引用,对象还在堆里,并且可达不使用,就是在ThreadLocalMap的Entry里产生了一堆key为null的东西)

的确是这样的,但是这个问题,jdk的设计者在设计的时候就在一定程度上进行了缓解——我们在调用ThreadLocal的get/set/remove的时候,底层源码会自动地把这一堆key为null的东西删除了,以便下一次GC把value回收掉

(2)threadLocalHashCode到底用来做什么的

这个参数,我们是用来唯一确定一个ThreadLocal对象

但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性呢?在ThreadLocal类中,还包含了一个static修饰的AtomicInteger成员变量和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。

确定了唯一的ThreadLocal对象,threadLocalHashCode还作为确定当前线程的ThreadLocalMap的table数组的位置(table数组其实就是Entry数组)

为什么不直接用线程id来作为ThreadLocalMap的key?

这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?

而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分,所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。

3.来看看当ThreadLocalMap存在的时候,继续存储会发生什么事

        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;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

来看看关键代码——e = tab[i = nextIndex(i, len)]

从这里可以看出,如果产生了hash冲突,ThreadLocalMap采用的是再哈希的方式解决冲突的

一张图描绘Thread的存储结构

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值