java多线程之ThreadLocal源码分析

什么是ThreadLocal?
关于ThreadLocal的知识网上有很多,但参差不齐很片面,看了很多博客后发现有一篇写的很全面客观,贴出来大家可以自行观看:http://www.iteye.com/topic/103804
下面讲一下我自己的理解:线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。ThreadLocal不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。

ThreadLocal的结构

首先我们来看一下ThreadLocal这个类的结构:
这里写图片描述
可以看到除了ThreadLocal的一些方法和变量之外,还有两个静态内部类:ThreadLocalMap和SuppliedThreadLocal,其中ThreadLocal是实现的关键,我们来看一下他的结构:
这里写图片描述
ThreadLocalMap里还有一个内部类Entry,其实每个线程存的值都在这里实现:
这里写图片描述

ThreadLocal实例

如果只是单纯的讲这个类难免枯燥,我们结合一个实例来讲;比如我们要在一个类中放一个String类型的字符串,让每个线程都能设置和获得不同的字符串,我们可以这样写:

public class Test {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();


    static class Thread1 extends Thread{

        @Override
        public void run() {
            //在新线程中设置值
            threadLocal.set("a");
            System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
        }
    }

    public static void main(String[] args) {
        //在主线程中设置值
        threadLocal.set("main");

        Thread1 thread1 = new Thread1();
        thread1.start();

        System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
    }
}

首先在Test类中创建一个ThreadLocal对象,指定泛型类型为String,然后在Test类中创建一个静态内部类Thread1,在他的run方法中给ThreadLocal设置值,然后取出值。在mian方法中首先给ThreadLocal设置一个值,然后开启Thread1线程,运行结果如下:

线程:Thread[main,5,main]值:main
线程:Thread[Thread-0,5,main]值:a

可以看到主线程中设置的值和主线程中打印的值是相对应的,而新线程中设置的值和新线程中打印的值是相对应的,这就是ThreadLocal。

实例分析

下面我们对这个实例进行分析,看ThreadLocal是如何在不同的线程中取出不同的值的:
在我们new出一个Threadlocal实例时,ThreadLocal做了那些操作呢,我们看一下他的构造函数:

    /**
     * Creates a thread local variable.
     */
    public ThreadLocal() {
    }

我们可以看到他的构造函数是空的,也就是说当我们new一个实例时只是初始化了成员变量,我们看一下他的静态代码和成员变量:

private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

其中有只有一个threadLocalHashCode是在创建对象时初始化的,因此ThreadLocal实例的变量只有这个threadLocalHashCode,而且是final的,用来区分不同的ThreadLocal实例,至于为什么要这样一个变量后面会详细说。

接下来通过threadLocal.set(“main”)来设置值,这个方法很关键:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

首先获取到当前线程的实例,然后在通过 getMap(t)方法获取到与线程绑定的ThreadLocalMap实例,如果获取到的实例不为空,则通过ThreadLocalMap的set方法设置值,否则创建一个ThreadLocalMap实例。
我虽然只用几句话总结了这个方法,但其实里面的具体实现还是挺复杂的,我们来看一下 getMap(t)和createMap(t, value)这两个方法:

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

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

getMap是返回一个ThreadLocalMap实例,而createMap是创建一个ThreadLocalMap实例,注意看创建的实例是放在那里的,t.threadLocals也就是Thread里的变量,我们看一下Thread类里的这个属性:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

这下就清楚了,我们保存值的时候,首先会通过Thread.currentThread()获取当前线程的实例,然后通过这个实例的getMap(t)方法获取线程里存放的ThreadLocalMap对象,如果当前线程里的ThreadLocalMap对象为空则创建一个实例放到该线程中,这样每个线程都拥有一个不同的ThreadLocalMap实例,而这个实例就是不同线程保存不同值的关键。搞清了这一点就基本了解ThreadLocal的原理了。
接着我们在看一下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);
        }

首先初始化一个Entry数组,关于Entry其实他就相当于一个键值对,ThreadLocal是他的键(key),我们存放的值是他的值(values),原来我们的值最终是存到了这个Entry中了。然后通过ThreadLocal的threadLocalHashCode 属性计算出一个值,还记得这个变量吗,其实他的作用就是映射一个ThreadLocal在Entry数组中的位置,由于每个Threadlocal的threadLocalHashCode值是不一样的,这样当我们取值的时候,通过这个threadLocalHashCode 就可以找到我们存储的Entry对象的位置,这里也许有人会好奇了,为什么不直接通过key也就是ThreadLocal在数组中查询呢,hashMap不就是这样做的吗,其实hashMap底层也是使用的hash表来查询的,这样做的好处是查询快。我们通过threadLocalHashCode计算出了当前ThreadLocal的Entry对象在Entry数组中的位置,接着我们就把Entry对象初始化然后放到数组的对应位置。
为什么要用一个Entry数组呢?直接将值存到一个Entry对象中,然后ThreadLocalMap持有这个实例不就行了吗?刚开始我也在这里纠结了好久,但是仔细一想,如果我们线程要存两个值,但是一个线程只有一个ThreadLocalMap实例,这显然就不行了,所以要用Entry数组,将不同的值存到数组中,例:

public class Test {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();


    static class Thread1 extends Thread{

        @Override
        public void run() {
            //在新线程中设置值
            threadLocal.set("a");
            threadLocal2.set(1);
            System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
            System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal2.get());
        }
    }

    public static void main(String[] args) {
        //在主线程中设置值
        threadLocal.set("main");
        threadLocal2.set(2);
        Thread1 thread1 = new Thread1();
        thread1.start();

        System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
        System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal2.get());
    }
}

运行结果:

线程:Thread[main,5,main]值:main
线程:Thread[Thread-0,5,main]值:a
线程:Thread[main,5,main]值:2
线程:Thread[Thread-0,5,main]值:1

现在还差一个map.set(this, value)方法:

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

这个方法其实就是省去了初始化Entry数组的过程,直接通过threadLocalHashCode计算出了当前ThreadLocal的Entry对象在Entry数组中的位置。

这里我们先总结几个知识点:

  1. ThreadLocal的构造函数是的,在初始化的时候只初始化一个threadLocalHashCode变量,这是唯一标识当前ThreadLocal对象的
  2. ThreadLocalMap的实例是存放在Thread中的,ThreadLocalMap的构造函数接收两个参数:ThreadLocal和values
  3. 我们存的值最终是以键值对的形式存在Entry对象中的,ThreadLocal是键,values是值
  4. ThreadLocal进行set操作时会先获取当前线程实例中的ThreadLocalMap对象,然后将值设置到ThreadLocalMap中存储的Entry数组的Entry对象中。
  5. Entry对象存到Entry数组中的位置是由threadLocalHashCode计算得到的,这样做是为了节省查找时间,不懂的百度一下hash表
  6. Entry数组是为了实现一个线程存储多个值

下面我们看一下threadLocal.get()方法:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

get方法也是要先获取当前线程的实例,然后在通过 getMap(t)方法获取到与线程绑定的ThreadLocalMap实例,如果获取到的实例不为空则通过这个ThreadLocalMap的getEntry()方法获取Entry实例,然后直接就获取了Entry实例中的值就可以了,如果ThreadLocalMap为空就初始化一个null值返回。
下面我们来详细分析一下get方法, getMap(t)不用讲了,和之前set里的一样,我们来看一下 map.getEntry(this)方法:

        private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

这里就用到threadLocalHashCode 来获取Entry对象在Entry数组中的位置,只是简单的数学计算就可以完成,可以说根本不用时间,但是如果一个个取出Entry对象再一个个比较他们的key,这浪费的时间可想而知,取出Entry对象后判断是否为空,并且判断一下取出的key是否是和当前ThreadLocal一样 ,判断完成后返回Entry对象就ok了,至于如果Entry为空或者key值不一样的处理大家自己去看getEntryAfterMiss(key, i, e)的源码,我就不讲了,这种情况一般不会出现。

我们再看一下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);
        return value;
    }

这个方法调用的条件是我们之前没有调用set方法给ThreadLocal设置值,这时是没有值能够取出来的,所以就调用一下该方法初始一个值,这个方法的第一行就是调用一个initialValue()方法,这个方法很重要,我们看一下他的实现:

    protected T initialValue() {
        return null;
    }

返回空,没错因为我们没有值只能返回空,注意这个方法是可以重写的,我们可以自定义他的默认值

    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "defaultValues";
        }
    };
    public static void main(String[] args) {
        //在主线程中设置值
//        threadLocal.set("main");
        threadLocal2.set(2);
        Thread1 thread1 = new Thread1();
        thread1.start();

        System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
        System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal2.get());
    }
线程:Thread[main,5,main]值:defaultValues
线程:Thread[main,5,main]值:2
线程:Thread[Thread-0,5,main]值:a
线程:Thread[Thread-0,5,main]值:1

在调用了initialValue()方法之后再次对ThreadLocalMap是否为空做一次判断,然后掉用 map.set(this, value)或者createMap(t, value)方法将我们初始化的值设置到Entry中。

这里再总结几个知识点:

  1. ThreadLocal进行get操作时也会先获取当前线程实例中的ThreadLocalMap对象,然后通过该对象获取Entry,进而获取到存储的值。
  2. ThreadLocal是有默认的值的,可以自定义也可以为空,默认的值也会存入Entry对象中。

到这里ThreadLocal的原理已经讲完了,可能有人会说这个东西根本没用到过,我们学他干嘛,我想说我们看源码更多的不是为了去使用它,如果只是ThreadLocal的使用我想几百字就解决了,我们是为了了解他的思想和解决问题的思路,这才是能提升我们的东西。

发布了65 篇原创文章 · 获赞 24 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/shanshui911587154/article/details/78707486