• 多线程知识梳理(5),当我们谈到ThreadLocal的时候,我们在谈什么?


    一、ThreadLocal是什么

    从名称来看ThreadLocal的直接翻译就是线程本地,可以粗糙的理解成当前现成的本地数据,是不和其他线程共享的数据。但是这么理解是不是太片面呢,这里我们看一下JDK源码对ThreadLocal的注释是什么吧。

    1. JDK源码说明

    /**
     * 
     * 这个类提供线程局部变量。这些变量与普通的变量不同,因为每个访问的线程(通过其get或set方法)都有
     * 自己的独立初始化的变量副本。ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态
     * (private static)字段(例如:一个用户ID或事务ID)。
     * 
     * 只要线程存活并且ThreadLocal实例可以访问,每个线程都保存对其线程局部变量副本的隐含引用; 
     * 线程结束之后,线程本地实例的所有副本都将被垃圾回收(除非存在对这些副本的其他引用)。
     * @author  Josh Bloch and Doug Lea
     * @since   1.2
     */
    

    2. 个人理解

    根据Jdk源码注释,我们可以得到以下理解:

    1. 每一个线程都在Threadlocal中单独存储,将ThreadLocal对象作为key,将存储的类型作为Value直接存储到ThreadLocalMap中;
    2. 由于是按照线程进行区分的,各个线程之间的变量不会互相影响;
    3. 因为ThreadLocal是和线程绑定的,如果是使用线程池的话,如果之前的线程没有在使用结束的时候执行remove操作,等到线程池再轮循到这个线程的时候,可能会读取到脏数据;
    4. 一个ThreadLocal在同一个线程中只能存储一个对象,如果多次执行set操作,后面存储的对象会覆盖前面存储的对象
    5. 由于这种kv的数据结构,我们可以粗略的将ThreadLocal理解成一个HashMap,只不过key是Threadlocal本身而已,有趣的时候,ThreadLocal在执行set的时候,也会执行自己的Hash寻址算法,这点和Hashmap很像。

    3. 使用场景

    如果你希望构造这样一个对象,将这个对象设置为共享变量,并统一设置初始值。但是你还希望每个线程对这个值的修改都是互相的独立的。那么这个对象就是ThreadLocal

    4. ThreadLocal和Thread的关系

    ThreadLocal有一个静态内部类叫ThreadLocalMap,它还有一个静态内部类Entry,在Thread中的ThreadLocalMap属性的赋值是在ThreadLocal类中的createMap中进行的。ThreadLocal和ThreadLocalMap有三组对应的方法:get、set和remove,在ThreadLocal中对它们只做校验和判断,最终的实现会落在ThreadLocalMap上。Entry继承自WeakReference,没有方法,只有一个value成员变量,它的key是threadLocal对象。

    // 静态内部类Entry
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    
    // Thread类中的Thread'Local'Map
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    简单梳理一下他们的关系:

    1. 1个Thread有且仅有一个ThreadLocalMap对象;
    2. 1一个Entry对象的Key弱引用指向1个ThreadLocal对象;
    3. 1个ThreadLocalMap对象可以存储多个Entry
    4. 1个ThreadLocal对象可以被多个线程所共享
    5. ThreadLocal对象不持有value,value由线程的entry对象持有

    所有Entry对象都被ThreadLocalMap类实例化对象threadLocals持有。当线程对象执行完毕时,线程对象内的实例属性均会被垃圾回收。但是,ThreadLocal对象经常被设置为私有静态变量使用,那么其生命周期至少不会随着线程结束而结束。

    二、ThreadLocal怎么用

    1. API

    返回值 方法名 备注
    void set 存储
    void remove 删除
    T get 获取

    2. 在多线程情况下的Demo

    启动类

    public class ThreadDemo {
    
    	// 声明一个ThreadLocal,存储类型为Integer
        public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    
        public static void main(String[] args) throws InterruptedException {
    		// 创建一个线程池大小为1的线程池
            Executor executor = Executors.newFixedThreadPool(1);
    		// 执行四个线程,这四个线程分别会先执行get在执行set操作
            for (int i = 0; i < 4; i++) {
                executor.execute(new RunnableDemo());
            }
    
            while (true) {
    //            System.out.println(threadLocal.get());
            }
        }
    
    }
    

    线程类

    public class RunnableDemo implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " get num : " + ThreadDemo.threadLocal.get());
            Integer num = new Random().nextInt();
            System.out.println(Thread.currentThread().getName() + " out num : " + num);
            ThreadDemo.threadLocal.set(num);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    输出结果:

    pool-1-thread-1 get num : null
    pool-1-thread-1 out num : 162662825
        
    pool-1-thread-1 get num : 162662825
    pool-1-thread-1 out num : -168394526
        
    pool-1-thread-1 get num : -168394526
    pool-1-thread-1 out num : 842018131
        
    pool-1-thread-1 get num : 842018131
    pool-1-thread-1 out num : 1188266731
    

    结论:

    由于我们的线程对象中没有对ThreadLocal执行remove方法,当线程池第二次轮询到这个线程的时候,直接执行threadLocal.get()方法获取到的还是上一次执行的结果。这种情况是要尽量避免,因为有可能因为没有执行remove操作,而导致第二次获取到的数据是错误的。

    3. ThreadLocal无法解决共享对象的更新问题

    如例子所示:

    public class ThreadLocalDemo {
        private static final StringBuilder INIT_VALUE = new StringBuilder("init");
        private static final ThreadLocal<StringBuilder> builder = new ThreadLocal<StringBuilder>() {
            @Override
            protected StringBuilder initialValue() {
                return INIT_VALUE;
            }
        };
    
        private static class AppendStringThread extends Thread {
            @Override
            public void run() {
                StringBuilder inThread = builder.get();
                for (int i = 0; i < 10; i++) {
                    inThread.append("-").append(i);
                }
                System.out.println(Thread.currentThread().getName() + inThread.toString());
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 10; i++) {
                new AppendStringThread().start();
            }
            TimeUnit.SECONDS.sleep(10);
        }
    }
    

    输出结果:

    Thread-1init-0-1-2-3-4-5-6-7-8-9
    Thread-3init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-0init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-2init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    

    可以看到输出的结构是乱序不可控的,所以使用讴歌引用来操作共享对象时,依然需要进行线程同步。

    现在我们将AppendStringThread类中的intThread计算来做上锁来保证线程之间的同步机制,其他方法不边。AppendStringThread具体代码如下:

    private static class AppendStringThread extends Thread {
        @Override
        public void run() {
            StringBuilder inThread = builder.get();
            ReentrantLock lock = new ReentrantLock(true);
            lock.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    inThread.append("-").append(i);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName() + inThread.toString());
        }
    }
    

    输出结果:

    Thread-0init-0-1-2-3-4-5-6-7-8-9
    Thread-1init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-2init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-4init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-6init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-7init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-8init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-9init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-3init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    Thread-5init-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9-0-1-2-3-4-5-6-7-8-9
    

    三、ThreadLocal 源码分析

    1. 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);
    }
    

    源码解析

    set方法内的东西看起来比较少,甚至我们不用看具体实现就能大概知道他想做什么。执行逻辑:

    1. 获取当前线程;
    2. 获取当前线程的ThreadLocalMap内部类对象;
    3. 判断ThreadLocalMap内部类是否为空;
    4. 如果不为空则执行set操作,将当前ThreadLocal作为key,value作为值存储到ThreadLocalMap中;
    5. 如果ThreadMap是null,则执行新建操作,并且将给定的ThreadLocal作为key,传入的对象作为value写入ThreadLocal中;

    具体是不是这样的呢,我们可以看一下代码。Thread t = Thread.currentThread();这段代码就不用解析了,因为不涉及到其他方法。我们直接来看第二行ThreadLocalMap map = getMap(t);

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

    可以看到返回的当前线程的threadLocals,那我们追过去看一下threadLocals是个什么东西呢?

    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    从这里我们可以看到所谓t.threadLocals本质上是指向了ThreadLocal类的一个内部类ThreadLocalMap,我们可以看一下ThreadLocalMap内部类中的属性

     		/**
             * The initial capacity -- MUST be a power of two.
             */
            private static final int INITIAL_CAPACITY = 16;
    
            /**
             * The table, resized as necessary.
             * table.length MUST always be a power of two.
             */
            private Entry[] table;
    
            /**
             * The number of entries in the table.
             */
            private int size = 0;
    
            /**
             * The next size value at which to resize.
             */
            private int threshold; // Default to 0
    

    是不是看起来似曾相识,是不是有点像HashMap。我们这边先暂时按下对ThreadLocalMap这个内部类的好奇之心,继续往下看。ThreadLocal.set方法后面发生了什么,它开始判断这个ThreadLocalMap是不是null,刚才我们看源码得到ThreadLocal.ThreadLocalMap threadLocals = null;可以得知,第一次访问的时候这个值一定是null,那么它就会触发createMap(t, value);方法。

    createMap(t, value);方法显然是一个初始化方法,我们看一下它做了什么:

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

    将当前线程的threadLocals的成员变量指向了一个新的ThreadMap对象,将当前ThreadLocal对象作为key,存储的对象作为value对ThreadLocalMap进行初始化

    接下来我们看一下ThreadLocalMap的构造器

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 将ThreadLocalMap的table属性初始化为一个长度为16的数组
        table = new Entry[INITIAL_CAPACITY];
        // 根据threadlocal对象进行hashcode和长度-1进行于预算,用来获取这个threadlocal放在数组中的位置,其实本质上这一步就是一个寻址算法,而且这寻址算法和hashmap的寻址算法及其相似,Hashmap中的寻址算法源码是这样的tab[i = (n - 1) & hash],为什么直接取模,是因为对于计算机来说这样的运算效率更高。
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 根据寻址算法找到的位置,将ThreadLocal作为key,存储的对象作为value封装成Entry对象存储到数组中
        table[i] = new Entry(firstKey, firstValue);
        // 将ThreadLocalMap中包含的元素个数修改为1
        size = 1;
        // 这个值类似于Hashmap中的threshold,是阈值,如果当前数组的大小大于它的时候就会触发rehash操作
        setThreshold(INITIAL_CAPACITY);
    }
    
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
    

    到现在为止,第一次初始化ThreadLocalMap的代码逻辑就已经全部梳理完了,现在我们看一下,第二次乃至第N次存储数据的时候,ThreadLocal是如何处理的

    if (map != null) // 第二次、第三次、第N次存储的时候,map肯定不是null,就会触发ThreadLocalMap的set方法
        map.set(this, value);
    
    private void set(ThreadLocal<?> key, Object value) {
    
      	// 获取当前Thread'Local'Map的table对象,将其赋值给局部变量tab
        Entry[] tab = table;
        // 获取长度
        int len = tab.length;
        // 获取当前ThreadLocal应该存储到的下表位置
        int i = key.threadLocalHashCode & (len-1);
    	// 进行循环,根据寻址算法给定的下表位置获取坐标,如果不是就继续循环下一个
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            // 获取entry的key,判断key是否和当前传入的key是同一个,如果是就覆盖value并结束set方法
            ThreadLocal<?> k = e.get();
            if (k == key) {
                e.value = value;
                return;
            }
    		// 如果当前的entry还没有初始化过值
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    	// 将tab下标为i的对象赋值为new Entry
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 判断是否需要扩容(见1.2) 判断增加这个entry之后,是否比阈值要大,如果比阈值要大就会进行rehash算法
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    

    注1.2

    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            // 判断i+1是否超过了tab的长度,如果超过了返回0,否则返回i+1
            i = nextIndex(i, len);
            Entry e = tab[i];
            // 如果entry不是null,但是没有Treadlocal作为key存储进去
            // 将长度传入的n设置为当前tab的长度
            // 将removed设置为ture
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }
    

    2. get方法

    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果当前线程的ThreadLocalMap为null或者当前的这个threadLocal对象在map中不存在,直接初始化
        return setInitialValue();
    }
    

    threadLocal的方法非常简单,寥寥几行。大概的意思就是判断一下当前线程的threadLocalMap是否为null,如果不是null,再判断当前这个threadlocal对象是否存储过数据,如果存储过就直接返回存储的数据,如果没有存储过。再执行初始化操作setInitialValue

    每个线程都有自己的ThreadLocalMap,如果map == null,则直接执行setInitialValue。如果map已经创建,则就表示Thread类的threadLocalMap属性已经初始化,如果e == null,依然会执行到setInitialValue。接下来我们看一下这个setInitialValue方法

    private T setInitialValue() {
        // 注1 这是一个保护方法,默认返回null,如果需要使用,需要覆写
        T value = initialValue();
        // 获取当前线程,并获取当前线程的ThreadLocalMap,如果为null则创建,否则直接写入
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    

    注1

    protected T initialValue() {
        return null;
    }
    

    这个方法默认是返回的null,如果大家希望再初始化value的时候,给定一个不同的值,那么就需要继承ThreadLocal并重写此方法。通常用于匿名内部类中,例如:

    	private static final ThreadLocal<Integer> INIT_DEMO = new ThreadLocal<Integer>(){
            @Override
            protected Integer initialValue() {
                return 1000;
            }
        };
        public static void main(String[] args) throws InterruptedException {
            System.out.println(INIT_DEMO.get());
        }
    

    上面代码中,就没有通过set方法给INIT_DEMO对象赋值,而是通过重写了initialValue方法,在INIT_DEMO对象调用get方法的时候给对象进行赋值的。

    3. remove方法

    public void remove() {
        // 获取当前线程的threadLocalMap
        ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果map不是null,直接将key为当前threadlocal对象的entry删除掉
        if (m != null)
            // 根据key删除entry
            m.remove(this);
    }
    

    remove方法应该是ThreadLocal中最简单的一个方法了,因为他不涉及到大量的方法调用,他就是获取到了当前线程的ThreadLocalMap对象,然后判断一下这个map是否为null,如果不是null,就尝试删除这个map中key为当前threadLocal的entry。下面是remove方法的源码

    private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
        // 寻址定位到这个key对应的下标位置
        int i = key.threadLocalHashCode & (len-1);
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            // 找到key了,直接清空这个entry
            if (e.get() == key) {
                e.clear();
                // 将这个key从数组中移除
                expungeStaleEntry(i);
                return;
            }
        }
    }
    

    四、ThreadLocal的副作用

    1. 脏数据

    线程复用会产生咱数据。由于线程池会重用Thread对象,那么与Thread绑定的静态属性ThreadLocal变量也会被宠用。如果在实现的线程的run方法中不显示地调用remove方法清理与线程相关的threadLocal信息,那么倘若下一个线程不调用set设置初始值,就可能get到重用的线程信息,包括threadlocal所关联的线程对象的value值。

    我们在【2.在多线程情况下的Demo】中就复现了这个问题,我们创建了一个线程池大小固定为1的线程池。然后将四个线程放入线程池执行。

    第一个线程执行完之后将162662825作为value存入threadLocal中,但是在线程的run方法中,并没有显示地调用remove方法。第一个线程执行完毕后,第二个线程开始执行。

    第二个线程在执行set之前,先执行了get方法,然后就获取到了上一个线程执行过程中set到threadlocal中的值,于是就出现了如下的结果:

    // 线程1
    pool-1-thread-1 get num : null
    pool-1-thread-1 out num : 162662825
    // 线程2
    pool-1-thread-1 get num : 162662825
    pool-1-thread-1 out num : -168394526
    

    2. 内存泄漏

    在源码注释中提示使用static关键字来修饰ThreadLocal。在这个场景下, 寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的value就不现实了。在上例中,如果不进行remove操作,那么这个线程回收完之后,通过ThreadLocal对象持有的String对象是不会被释放的。

    3. 解决方案

    其实以上两个问题解决方法很简单,就是在每次用完ThreadLocal时,必须要及时调用remove方法显示的清理。

    五、参考资料

    《Java并发编程实战》

    《码出高效 Java开发手册》

    《JDK1.8源码》

  • 相关阅读:
    Linux基础ls命令
    Linux基础tree命令
    Java银行调度系统
    Java交通灯系统
    Java反射
    Java基础IO流
    Java多线程
    Java集合框架
    Springmvc的一些属性功能
    JS
  • 原文地址:https://www.cnblogs.com/joimages/p/12834865.html
Copyright © 2020-2023  润新知