• Java并发包2--ThreadLocal的使用及原理浅析


    ThreadLocal 是本地线程变量,是一个以ThreadLocal对象为key,任意对象为value的存储结构。

    一、使用案例

    1.定义线程类MyThread,代码如下:

     1 public class MyThread extends Thread{
     2 
     3     private User user;
     4 
     5     public MyThread(User user){
     6         this.user = user;
     7     }
     8 
     9     public void run() {
    10         System.out.println("线程:"+Thread.currentThread().getName()+"设置ThreadLocal的user="+user.getUserName());
    11         ThreadLocalTest.LOCAL.set(user);
    12         try {
    13             Thread.sleep(2000L);
    14         } catch (InterruptedException e) {
    15             e.printStackTrace();
    16         }
    17         User user =  ThreadLocalTest.LOCAL.get();
    18        System.out.println("线程:"+Thread.currentThread().getName()+"从ThreadLocal获取的user="+user.getUserName());
    19     }
    20 }

    2.测试方法Main方法

     1 public class ThreadLocalTest {
     2 
     3     //全局ThraedLocal变量
     4     public static ThreadLocal<User> LOCAL = new ThreadLocal<User>();
     5 
     6     public static void main(String[] args){
     7         User user1 = new User();
     8         user1.setUserName("Jack");
     9         User user2 = new User();
    10         user2.setUserName("Bob");
    11 
    12         //定义两个线程变量
    13         Thread t1 = new MyThread(user1);
    14         Thread t2 = new MyThread(user2);
    15         t1.start();
    16         t2.start();
    17 
    18         //从ThreadLocal变量中获取数据
    19         User user = LOCAL.get();
    20         System.out.println(user==null);//当前线程为Main线程,而Main线程没有设置过TheadLocal的值,所以获取不到
    21         LOCAL.set(user2);
    22         System.out.println(LOCAL.get().getUserName());//从Main线程设置ThreadLocal,则可以获取
    23     }
    24 }

    定义两个线程,线程的run方法执行了ThreadLocal变量的set操作,然后再执行get操作,可以获取到本线程设置的值

    而直接从Main线程中执行ThreadLocal的get方法,返回的数据为null,只有在自己的线程中执行了set操作,才可以获取到值,

    上例的执行结果如下:

    1 true
    2 Bob
    3 线程:Thread-0设置ThreadLocal的user=Jack
    4 线程:Thread-1设置ThreadLocal的user=Bob
    5 线程:Thread-0从ThreadLocal获取的user=Jack
    6 线程:Thread-1从ThreadLocal获取的user=Bob

    二、源码解析

    ThreadLocal主要有三个方法,

    set (T value)  给ThreadLocal变量设置数据,ThreadLocal会存储当前线程存储的值

    get ( )   返回ThreadLocal当前线程设置的值

    remove() 删除当前线程设置的ThreadLocal的值

    2.1、set方法解析

    源码如下:

     1 public void set(T value) {
     2         // 获取当前线程
     3         Thread t = Thread.currentThread();
     4         // 获取当前线程的ThreadLocalMap
     5         ThreadLocalMap map = getMap(t);
     6         if (map != null)
     7             // 如果当前线程的ThreadLocalMap存在,则直接将当前的value设置到map中,map的key就是当前的ThreadLocal对象
     8             map.set(this, value);
     9         else
    10             // 如果当前线程的ThreadLocalMap不存在,则先创建ThreadLocalMap,则进行赋值
    11             createMap(t, value);
    12     }

    首先是通过getMap (Thread t) 方法获取当前线程的ThreadLocalMap对象,代码如下:

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

    每个线程Thread对象内部都有一个ThreadLocalMap对象threadLocals

    1 ThreadLocal.ThreadLocalMap threadLocals = null; //Thread类中持有一个ThreadLocalMap对象

    如果当前线程的ThreadLocalMap不存在,则只需createMap方法初始化,代码如下:

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

    直接初始化一个ThreadLocalMap,然后赋值给Thread的threadLocals对象

    可以发现ThreadLocal的set方法逻辑其实很简单,就是获取一个ThreadLocalMap对象,然后将需要set的值保存在ThreadLocalMap中

    ThreadLocalMap是ThreadLocal的一个内部类,如下:

    static class ThreadLocalMap {
            static class Entry extends WeakReference<ThreadLocal<?>> {
               
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
        }

    ThreadLocal的set方法实际是执行了ThreadLocalMap的set方法

     1 private void set(ThreadLocal<?> key, Object value) {
     2 
     3             // We don't use a fast path as with get() because it is at
     4             // least as common to use set() to create new entries as
     5             // it is to replace existing ones, in which case, a fast
     6             // path would fail more often than not.
     7 
     8             Entry[] tab = table;
     9             int len = tab.length;
    10             int i = key.threadLocalHashCode & (len-1);
    11 
    12             for (Entry e = tab[i];
    13                  e != null;
    14                  e = tab[i = nextIndex(i, len)]) {
    15                 ThreadLocal<?> k = e.get();
    16 
    17                 if (k == key) {
    18                     e.value = value;
    19                     return;
    20                 }
    21 
    22                 if (k == null) {
    23                     replaceStaleEntry(key, value, i);
    24                     return;
    25                 }
    26             }
    27 
    28             tab[i] = new Entry(key, value);
    29             int sz = ++size;
    30             if (!cleanSomeSlots(i, sz) && sz >= threshold)
    31                 rehash();
    32         }

    可以发现是ThreadLocalMap是将当前的ThreadLocal当做一个key,需要存储的对象为value,存储在ThreadLocalMap内部的Entry数组中。

    ThreadLocalMap也会存储hash冲突的问题,只是解决冲突的方式比较简单,指定尝试获取下一个位置用于存放,直到能够放入位置(ThreadLocal不建议一个线程有太多ThreadLocal,所以没必要花费大力气解决冲突问题)

    同理既然ThreadLocal的set方法是执行了ThreadLocalMap的set方法,那么可以猜想ThreadLocal的get方法也是执行了ThreadLocalMap的get方法。

    如下:

     1 public T get() {
     2         Thread t = Thread.currentThread();
     3         ThreadLocalMap map = getMap(t);
     4         if (map != null) {
     5             ThreadLocalMap.Entry e = map.getEntry(this);
     6             if (e != null) {
     7                 @SuppressWarnings("unchecked")
     8                 T result = (T)e.value;
     9                 return result;
    10             }
    11         }
    12         return setInitialValue();
    13     }

    ThreadLocalMap的getEntry方法如下:

    1  private Entry getEntry(ThreadLocal<?> key) {
    2             int i = key.threadLocalHashCode & (table.length - 1);
    3             Entry e = table[i];
    4             if (e != null && e.get() == key)
    5                 return e;
    6             else
    7                 return getEntryAfterMiss(key, i, e);
    8         }

    为什么ThreadLocalMap是采用数组存储的呢?因为ThreadLocalMap是和Thread绑定的,一个Thread只有一个ThreadLocalMap对象,但是每个Thread可以存储多个ThreadLocal对象

    所以ThreadLocalMap中的数组就是存储了多个ThreadLocal对象,数组的下标是通过 threadLocal对象的hashCode和数组长度进行取模算法得到的数组下标值。

    分析到这里可以总结出ThreadLocal的原理:

    每个线程Thread 内部有一个 ThreadLocalMap对象,每个ThreadLocalMap 存储了多个以 ThreadLocal对象为key,存储的数据为value的值。ThreadLocalMap内部采用数组存储ThreadLocal的值

    通过ThreadLocal的hashCode和数组长度进行取模算法得到数组的下标位置,在指定的位置存储ThreadLocal的值。

    分析到这里涉及到的对象 包含了 Thread、ThreadLocal 和 ThreadLocalMap,这三者的关系为:

    Thread 内部有一个 ThreadLocalMap对象实例 

    ThreadLocalMap  是 ThreadLocal的一个内部类

    ThreadLocalMap 存储的是以 ThreadLocal 为key,ThreadLocal的值为value 的结构

    光靠文字不好理解,使用图形的话很更直观,以上面的代码为例,三者关系如下图示:

     

     每个Thread中的ThreadLocalMap可以存储多个ThreadLocal的值,但是同一个ThreadLocal变量在同一个Thread中只能保存一个值,后面set的值会把前面set的值覆盖。

    如果需要保存多个值就需要使用多个ThreadLocal来存储

    三、ThreadLocal内存泄露浅析

    ThreadLocal很好的解决了线程之间数据隔离,但是大量的ThreadLocal在大量的线程中就有了空间的问题,内存中存储的ThreadLocal值的个数等于 ThreadLocal变量个数 * 线程个数,显然随着线程的变多,ThreadLocal占据的空间也是不容小觑的。

    所以使用ThreadLocal的时候就需要做到不用对时候及时回收,防止没有被回收导致内存泄露问题。如下图示:

    栈中有一个ThreadLocal的变量和一个Thread的变量 分别强引用堆中对应的实例,堆中的ThreadLocalMap实例也是被Thread变量引用

    ThreadLocalMap又持有 Entry实例的强引用,而Entry分别持有key的弱引用,value的强引用,key是 ThreadLocal实例,value是实际存的数据

    上图中实现表示强引用,虚线表示弱引用。

    当ThreadLocal 不用的时候,ThreadLocal变量会被回收,此时ThreadLocal 变量和ThreadLocal实例的强引用断开,但是此时ThreadLocal实例还不能够被回收,引用还有 Entry的key对ThreadLocal实例持有着弱引用

    所以当下一次执行GC的时候,ThreadLocal实例就会被回收,因为GC的时候会直接回收弱引用实例。此时Entry的key已经被回收了,所以Entry就变成了<null, value >的结构,这就会导致这个value是无法被获取了。

    如果这个Thread 一直活跃,那么Thread 强引用 ThreadLocalMap;ThreadLocalMap强引用Entry,Entry强引用value就都不能被回收,所以一旦ThreadLocal被回收,而Thread还继续工作的话,就会导致value无法被访问,

    从而就造成了 内存泄露问题。

    既然Entry的 key是弱引用,那么为什么value不是弱引用呢?

    如果value是弱引用的话,那么在GC的时候就会被回收,而ThreadLocal除了有弱引用,还有一个强引用,所以在GC的时候ThreadLocal并不会被强制回收,而value会被回收,就会出现通过 key无法获取到数据的情况,

    如果GC频繁,那么ThreadLocal的get方法就会频繁获取不到数据,那么这样的ThreadLocal还有什么意义呢?

    所以ThreadLocalMap的实现仅仅将key作为了弱引用,value不会出现弱引用,这样虽然有内存泄露问题,但是至少可以保证只要 ThreadLocal 还在存活状态,就可以获取到value,显然是高可用的。

    那么既然有内存泄露问题,ThreadLocal就不管了么?显然不可能!

    ThreadLocal的get方法、set方法、remove方法的内部都做了判断,如果存储key=null都情况,就将value设置为null,一旦value设置为null之后,那么就将value和实际的数据之前的强引用断开了,那么数据在GC的时候就会被回收

    但是这样的设计就导致ThreadLocal 如果再也不会执行get、set 或 remove方法了,那么还是会存在内存泄露问题,所以在使用的时候需要养成好习惯,当不用ThreadLocal的时候,手动执行remove方法回收数据。

  • 相关阅读:
    枚举EnumHelper
    日期相关转化 TimeHelper
    C# 环境变量设置
    PDF 文件操作 PdfHelper
    32位系统支持多大内存?
    我们对待西洋近代文明的态度
    深度剖析CPython解释器》Python内存管理深度剖析Python内存管理架构、内存池的实现原理
    Element2.15.6 版本 Carousel 走马灯,当循环项长度为2时,循环动画的运行方向不能始终一致的问题处理
    python globals()[]将字符串转化类,并通过反射执行方法
    Pycharm import faker 和 colorlog提示“No module name faker/colorlog”
  • 原文地址:https://www.cnblogs.com/jackion5/p/11267436.html
Copyright © 2020-2023  润新知