ThreadLocal的学习

ThreadLocal介绍

ThreadLocal,顾名思义,线程局部变量。对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。

我们可以把它看作是一个改装过的一个类,我们假设现在有一个这个类的公共实例变量,有好几个线程它们都能访问使用这个变量。这个变量有以下几个常用的方法:

1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。

2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。

3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。

4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

好的现在假设各个线程都给这个变量 set了一个自己的int数字,如果是普通的变量,正常的逻辑应该是:哪个线程是最后一个设置它的,这个变量的值就是哪个。

但现在这个改装过的不一样,虽然各个线程都可以操作这个变量,但对各个线程来说,它其实是逻辑上独立的。换句话说,即使刚刚各个线程都给它set了,但每个线程get的时候,拿到的还是自己本来set的那个int值。

看个例子来理解吧:

public class MyThreadLocal 

{

    private static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(){

        /**

         * ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值

         */

        @Override

        protected Object initialValue()

        {

            System.out.println("调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!");

            return null;

        }

    };

     

    public static void main(String[] args)

    {

        new Thread(new MyIntegerTask("IntegerTask1")).start();

        new Thread(new MyStringTask("StringTask1")).start();

        new Thread(new MyIntegerTask("IntegerTask2")).start();

        new Thread(new MyStringTask("StringTask2")).start();

    }

     

    public static class MyIntegerTask implements Runnable

    {

        private String name;

         

        MyIntegerTask(String name)

        {

            this.name = name;

        }

 

        @Override

        public void run() 

        {

            for(int i = 0; i < 5; i++)

            {

                // ThreadLocal.get方法获取线程变量

                if(null == MyThreadLocal.threadLocal.get())

                {

                    // ThreadLocal.et方法设置线程变量

                    MyThreadLocal.threadLocal.set(0);

                    System.out.println("线程" + name + ": 0");

                }

                else

                {

                    int num = (Integer)MyThreadLocal.threadLocal.get();

                    MyThreadLocal.threadLocal.set(num + 1);

                    System.out.println("线程" + name + ": " + MyThreadLocal.threadLocal.get());

                    if(i == 3)

                    {

                        MyThreadLocal.threadLocal.remove();

                    }

                }

                try

                {

                    Thread.sleep(1000);

                }

                catch (InterruptedException e)

                {

                    e.printStackTrace();

                }

            }   

        }

         

    }

     

    public static class MyStringTask implements Runnable

    {

        private String name;

         

        MyStringTask(String name)

        {

            this.name = name;

        }

 

        @Override

        public void run() 

        {

            for(int i = 0; i < 5; i++)

            {

                if(null == MyThreadLocal.threadLocal.get())

                {

                    MyThreadLocal.threadLocal.set("a");

                    System.out.println("线程" + name + ": a");

                }

                else

                {

                    String str = (String)MyThreadLocal.threadLocal.get();

                    MyThreadLocal.threadLocal.set(str + "a");

                    System.out.println("线程" + name + ": " + MyThreadLocal.threadLocal.get());

                }

                try

                {

                    Thread.sleep(800);

                }

                catch (InterruptedException e)

                {

                    e.printStackTrace();

                }

            }

        }

         

    }

<strong>}

</strong>
View Code
 
输出结果是:
调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!

线程IntegerTask1: 0

调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!

线程IntegerTask2: 0

调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!

调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!

线程StringTask1: a

线程StringTask2: a

线程StringTask1: aa

线程StringTask2: aa

线程IntegerTask1: 1

线程IntegerTask2: 1

线程StringTask1: aaa

线程StringTask2: aaa

线程IntegerTask2: 2

线程IntegerTask1: 2

线程StringTask2: aaaa

线程StringTask1: aaaa

线程IntegerTask2: 3

线程IntegerTask1: 3

线程StringTask1: aaaaa

线程StringTask2: aaaaa

调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!

线程IntegerTask2: 0

调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!

线程IntegerTask1: 0

可以看到,虽然只有一个类变量threadlocal,但对各个线程来说,好像是它们自己的局部变量一样,互相不影响。

threadLocal原理

在介绍ThreadLocal的原理之前,先要介绍几个类

首先,这个threadLocal是和各个线程相关的,所以可以想象,这个ThreadLocal的原理肯定和Thread类有关系。

在Thread类中,有个类变量

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

这个类变量很容易理解的,是这个Thread对象所代表的线程的ThreadLocalMap。这是个Map,里面的每个entry的key是ThreadLocal实例对象。

而这个ThreadLocalMap类呢,是ThreadLocal类的一个嵌套类(就static内部类)。

从set方法的源码来看原理

先说说大概思路吧,当为一个ThreadLocal的实例变量,set的时候,首先会获得当前线程(因为是它自己去获得当前线程,所以这个线程是它自己本身),然后拿到这个线程的ThreadLocalMap。然后再以这个要set的ThreadLocal实例为key,set的东西为value放进到这个ThreadLocalMap里面去。

来看具体源码

set方法:

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();//获得当前线程,也就它自己
        ThreadLocalMap map = getMap(t);//获得这个线程的ThreadLocalMap,下面贴了这个方法的代码
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }



 /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

我们看到,如果这个threadLocalMap存在,就交给这个map去执行set方法。

既然是map的set,我们要关心的肯定是在map中table中的桶序号是怎么生成的:

 Entry[] tab = table;
 int len = tab.length;
 int i = key.threadLocalHashCode & (len-1);

和hashMap一样,用key得到的一个hash值然后与上长度减一。

所以关键看这个threadLocalHashCode,要看懂这个,还要介绍下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);
}

 首先,对于每个ThreadLocal对象,都有一个final的threadLocalHashCode,这是不能变的。

对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。
  

但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:

  在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性

set的分析就大概到这,理解了主要原理就好,就不深入了。

最后再捋一下这几个类的关系和原理(有点乱):

ThreadLocal类里面有个static内部类——ThreadLoalMap,这个map中的key是ThreadLocal的实例自己,value是set的object。

Thread类里面有个类变量——ThreadLocalMap threadLocals。

所以调用threadLocal的set方法的时候,从当前线程中拿到它的Thread实例,然后从中拿threadLocalMap,然后再根据这个threadLocal实例生成一个hash值定位到那个entry巴拉巴拉。

关于ThreadLoca的内存泄露问题

先来看ThreadLocalMap这个Map中定义的Entry,也就是存在map中的对象。

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

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

这个Entry是继承弱引用的!!

Object是直接用强引用来指,然后key是用weak来指。

所以下面来看使用一个ThreadLocal的时候的引用、对象关系图:

(图来自https://www.cnblogs.com/xzwblog/p/7227509.html)

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。

ThreadLocalMap在设计的时候也想到这个问题,于是想出了一些对策:

在调用ThreadLocal的get和set方法的时候,会帮你删掉key为null的那些entry,删掉后,entry中的value也没有了强引用,自然会被gc掉。

但光是这样是不足够的,所以保险起见,在使用ThreadLocal的时候最好:

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;

2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。

使用场景

ThreadLocal的主要用途是为了保持线程自身对象和避免参数传递,主要适用场景是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

例子:

数据库连接,最好每个线程有自己的数据库连接。——https://www.2cto.com/kf/201805/750397.html这个例子挺简单易懂的。

session相关的。

参考文章

https://www.cnblogs.com/xzwblog/p/7227509.html——《彻底理解ThreadLocal》这个原理讲得清楚很多。

https://www.cnblogs.com/coshaho/p/5127135.html——《ThreadLocal用法详解和原理 》这个例子把ThreadLocal的用法讲的很清晰。

https://www.2cto.com/kf/201805/750397.html——《什么是ThreadLocal?ThreadLocal应用场景在哪?》一个数据库连接的使用场景的例子。

原文地址:https://www.cnblogs.com/wangshen31/p/10453609.html