zoukankan      html  css  js  c++  java
  • 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应用场景在哪?》一个数据库连接的使用场景的例子。

  • 相关阅读:
    Linux上天之路(十七)之Shell编程二
    Linux上天之路(十六)之Shell编程一
    Linux上天之路(十五)之文件查找
    Elasticsearch搜索调优权威指南 (1/3)
    【大数据】SparkSql 连接查询中的谓词下推处理 (二)
    【大数据】SparkSql 连接查询中的谓词下推处理 (一)
    搜索和其他机器学习问题有什么不同?
    Searching with Deep Learning 深度学习的搜索应用
    翻译 | The Principles of OOD 面向对象设计原则
    400+节点的 Elasticsearch 集群运维
  • 原文地址:https://www.cnblogs.com/wangshen31/p/10453609.html
Copyright © 2011-2022 走看看