ThreadLocal的使用及其原理

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

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

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

ThreadLocal使用方式

直接看一段代码:

public class ThreadLocalTest {

    private static ThreadLocal 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 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> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

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

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

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

(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的存储结构

ThreadLocal的使用及其原理_第1张图片

 

 

 

 

 

 

 

 

你可能感兴趣的:(java)