ThreadLocal线程局部变量浅析

版权声明:欢迎转载大宇的博客,转载请注明出处: https://blog.csdn.net/yanluandai1985/article/details/82492398

一、ThreadLocal的基本定义

        官方定义:当使用 ThreadLocal 维护(set)变量时,ThreadLocal 为每个使用该变量的线程提供(get)独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

        ThreadLocal 并不是一个 Thread,而是 线程Thread的一个局部变量。

        什么是变量?从字面意思就知道是“会变化的数据”。ThreadLocal是线程变量,也就意味着它专门用于线程Thread对象,且在不同的线程对象里面代表着不同的值。

        所以,ThreadLocal翻译为线程局部变量,更为贴切。

        线程映射值,那么其实可以把它视为键值对Map。实际上,在ThrealLocal内部用的就是Map。

        线程任务执行完毕后,线程对应的局部变量会被GC回收。

        平时我们经常用到ThreadLocal 的方法是 get()、set()、remove()、initValue()。

        下面模拟一个简单的ThreadLocal

        

import java.util.*;

/**
 * Created by jay.zhou on 2018/9/7.
 */
public class MyThreadLocal<T> {
    //内部采用Map<Thread,T>实现,让Thread 映射 线程局部变量
    private Map<Thread, T> map = Collections.synchronizedMap(new HashMap<>());

    public T get() {
        //创建值的引用
        T value;
        //获取当前线程
        Thread currentThread = Thread.currentThread();
        //先从map中获取值
        value = map.get(currentThread);
        //如果值是空,说明是第一次调用此方法,将其放入我们的键值对中
        if (value == null) {
            //获取定义的初始值
            value = initValue();
            //存入内部键值对中
            map.put(currentThread, value);
        }
        return value;
    }

    public void set(T value) {
        //获取当前线程
        Thread currentThread = Thread.currentThread();
        //存入内部键值对中
        map.put(currentThread, value);
    }

    //移除此线程局部变量当前线程的值
    public void remove() {
        //获取当前线程
        Thread currentThread = Thread.currentThread();
        //移除键值对
        map.remove(currentThread);
    }

    //初始值,可以被子类重写的方法
    public T initValue() {
        return null;
    }


    public static void main(String[] args) throws InterruptedException {
        MyThreadLocal<List<String>> threadLocal = new MyThreadLocal<>();
        //线程标准写法
        new Thread(() -> {
            List<String> list = Arrays.asList("a", "b", "c");
            threadLocal.set(list);
            System.out.println(Thread.currentThread().getName());
            threadLocal.get().forEach(param -> System.out.println(param));
            //分割线
            System.out.println("--------");
        }).start();
        Thread.sleep(100);
        new Thread(() -> {
            List<String> list = Arrays.asList("d", "e", "f");
            threadLocal.set(list);
            System.out.println(Thread.currentThread().getName());
            threadLocal.get().forEach(param -> System.out.println(param));
        }).start();
    }
}
/**
   Thread-0
   a
   b
   c
------------
   Thread-1
   d
   e
   f
 *
/

二、ThreadLocal的源代码探索

        

        在学习了ThreadLocal类的源码以后,我才发现,原来ThreadLocal没有上面那么简单。

        首先,既然是线程的局部变量,那么肯定是存放在线程Thread里面的

        查看Thread类的源代码可以找到存放局部变量的代码,其实就是它的内部Map集合ThreadLocalMap

public
class Thread implements Runnable {
    //省略其它代码

    ThreadLocal.ThreadLocalMap threadLocals = null;
}

        原来,在Thread类中,持有了一个ThreadLocalMap集合。这个ThreadLocalMap集合的内部,是通过Entry这个内部类对象来存储数据的。这与HashMap和LinkedList实现原理十分相似:用对象来封装值。

        Entry对象的构造函数在下面展示出来了。接收的是ThreadLocal与Object value。因此,Entry可以视为真正存储数据的地方。一个ThreadLocalMap里面有多个Entry,因为某个线程不可能只有一个变量吧。

static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 当前ThreadLocal对应的值 */
            Object value;
            //通过构造函数接收键值对 ThreadLocal--value,存储到具体的Entry对象中
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

         综上,Thread、ThreadLocalMap、Entry(或者说ThreadLocal : Value键值对)的关系就很简单了。

è¿éåå¾çæè¿°

         一个Thread对象中持有一个的ThreadLocalMap。ThreadLocalMap中持有多个Entry键值对。键为具体的ThreadLocal对象local,值为local.set( Object value)的这个value对象。

         在某个线程的任务代码中,调用ThreadLocal的set方法的时候,是怎么跟当前线程联系起来的呢?

public class ThreadLocal<T> {

   public void set(T value) {
        //获取当前线程,原来是通过这种方式与当前线程取得联系
        Thread t = Thread.currentThread();
        //获取到当前线程对象的ThreadLocalMap对象,因为ThreadLocalMap对象是默认的访问权限,在同包里是可以调用到的
        ThreadLocalMap map = t.threadLocals;
        //如果map不为空,说明之前调用过此set()方法,所以创建了ThreadLocalMap对象。
        //没调用的话,那么就新创建ThreadLocalMap对象
        if (map != null)
            //把当前的threadLocal与value组成键值对Entry,放入ThreadLocalMap中
            //此map.set(..)方法设计到内存回收,回收key为null的value的内存
            map.set(this, value);
        else
            //看,如果是第一次调用set方法,那么会创建ThreadLocalMap对象。
            createMap(t, value);
    }

    //创建ThreadLocalMap的逻辑非常简单,就是把新创建的ThreadLocalMap与当前线程t联系起来。
    void createMap(Thread t, T firstValue) {
        //总会调用这一步的,因为总有第一次调用ThreadLocal.set()方法。
        //所以,把新创建的ThreadLocalMap绑定到当前线程 currentThread.threadLocals 变量上
        //这里的键值对  this代表threadLocal对象,值就是set(firstValue)方法里面的参数firstValue
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

}

         现在,某个线程Thread的任务里面,已经执行了threadLocal.set(value); 了。那么,怎么才能取出来值呢。

         我们调用的是Object value = threadLocal.get(); 是用来获取值的代码。调用5秒钟,分析两小时

         我们知道,哪个线程执行了这个代码,哪个线程就是所谓的“当前线程”。

        (1)先拿到当前线程

        (2)再获取当前线程中的 ThreadLocalMap对象

        (3)通过当前ThreadLocal 对象 ,在 ThreadLocalMap对象中去找 ,找到那组键值对 Entry对象《ThreadLoacl,Value》

        (4)最终通过 Entry.Value ,返回我们要的值。

public class ThreadLocal<T>{

        public T get() {
        //因为值都存放到当前线程里面了,所以要去当前线程里面拿嘛
        Thread t = Thread.currentThread();
        //获取到当前线程对象的ThreadLocalMap对象
        ThreadLocalMap map = t.threadLocals;
        //如果map不为空,说明之前当前线程的任务代码里面之前调用过ThreadLocal的set()方法
        if (map != null) {
            //Entry是个键值对,键是ThreadLocal。通过ThreadLocal对象肯定能拿到那个键值对对象Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果有值,那么就返回这个值。没值,那就返回初始值咯
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果在set() 方法调用之前就调用get(),那么只好返回 initValue()方法的值咯
        return setInitialValue();
    }

   public void set(T value) {
        //获取当前线程,原来是通过这种方式与当前线程取得联系
        Thread t = Thread.currentThread();
        //获取到当前线程对象的ThreadLocalMap对象,因为ThreadLocalMap对象是默认的访问权限,在同包里是可以调用到的
        ThreadLocalMap map = t.threadLocals;
        //如果map不为空,说明之前调用过此set()方法,所以创建了ThreadLocalMap对象。
        //没调用的话,那么就新创建ThreadLocalMap对象
        if (map != null)
            //把当前的threadLocal与value组成键值对Entry,放入ThreadLocalMap中
            map.set(this, value);
        else
            //看,如果是第一次调用set方法,那么会创建ThreadLocalMap对象。
            createMap(t, value);
    }

    //创建ThreadLocalMap的逻辑非常简单,就是把新创建的ThreadLocalMap与当前线程t联系起来。
    void createMap(Thread t, T firstValue) {
        //总会调用这一步的,因为总有第一次调用ThreadLocal.set()方法。
        //所以,把新创建的ThreadLocalMap绑定到当前线程 currentThread.threadLocals 变量上
        //这里的键值对  this代表threadLocal对象,值就是set(firstValue)方法里面的参数firstValue
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

}

三、ThreadLocal导致的内存溢出

         我们知道,使用线程池,关闭线程变成向线程池归还线程。所以,线程是一直存活的。

         ThreadLocal存放的值,会放到线程的ThreadLocalMap中。如果每次任务都往内部存储一个线程局部变量,且这个变量可能一个大对象,次数多了以后,可能会造成内存溢出。

         当然了,正常情况下,在线程被关闭以后,它的内部的ThreadLocalMap对象也一同被GC回收,再内部的Entry对象也被回收了,所以不使用线程池的情况几乎不可能造成内存溢出。

      

四、结论

        (1)在某个具体的线程任务中,调用ThreadLocal.set(Object object)方法,其实就是用Entry封装数据。new Entry(threadLocal , object)。然后把这个Entry对象存放到 这个线程的 thread的ThreadLocalMap中。因此,可以说,ThreadLocal 只是操作 Thread 中的 ThreadLocalMap 对象的集合。 通过set存储数据到集合中,通过get从集合中获取数据。

        (2)线程中的 ThreadLocalMap 变量的值是在 ThreadLocal 对象进行 set 或者 get 操作时创建的,原本是在Thread对象中是null。准确的说是,第一次调用ThreadLocal.set()方法,将会为当前线程创建ThreadLocalMap集合。

        (3)当前线程Thread对象的ThreadLocalMap 的键,其实就是 ThreadLocal对象

        (4)当ThreadLocalMap内部的Entry个数超过阙值的2/3,map开始扩容并且清理部分Entry的内存

        (5)其实,之所以各个线程之间不会出现干扰的原因就是,在线程内部创建了一个新的Entry对象,以“空间换时间”。

.

猜你喜欢

转载自blog.csdn.net/yanluandai1985/article/details/82492398