Java并发基础七:深入理解ThreadLocal

ThreadLocal是什么?

ThreadLocal是线程本地变量,可以为多线程的并发问题提供一种解决方式,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal使用场景

①多个线程去获取一个共享变量时,要求获取的是这个变量的初始值的副本。②每个线程存储这个变量的副本,对这个变量副本的改变不去影响变量本身。③适用于多个线程依赖不同变量值完成操作的场景。

ThreadLocal类常用接口

void set(T value):设置当前线程的线程局部变量的值

T get():获取当前线程所对应的线程局部变量

void remove():删除当前线程局部变量的值,目的是为了减少内存的占用

T initialValue():该线程局部变量的初始值(默认值为null),该方法是一个protected的懒加载方法,线程第1次调用get()或set(T value)时才执行在,而且也是为了让子类覆盖而设计的。

使用案例:

Demo①:ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

public class ThreadLocalDemo {
   private static ThreadLocal<Index> index = new ThreadLocal(){
       @Override
       protected Object initialValue() {
           return new Index();
      }
  };
   private static class Index{
       private int num;

       public void incr(){
           num++;
      }
  }
   public static void main(String[] args) {
       for(int i=0; i<5; i++){
           new Thread(() ->{
               Index local = index.get();
               local.incr();
               System.out.println(Thread.currentThread().getName() + " " + index.get().num);
          }, "thread_" + i).start();
      }
  }
}

输出结果:

thread_1 1

thread_0 1

thread_3 1

thread_4 1

thread_2 1

Demo②:SimpleDateFormat是非线程安全(共享变量calendar访问没有做到线程安全),ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么自然也就不存在竞争问题了。

ThreadLocal工作原理

ThreadLocal内部维护的是一个类似Map的ThreadLocalMap数据结构,而每个Thread类,都有一个ThreadLocalMap成员变量。ThreadLocalMap将线程本地变量(ThreadLocal)作为key,线程变量的副本作为value,如图所示:

实现原理:ThreadLocal底层实现是ThreadLocalMap数据结构,当使用ThreadLocal维护变量时,ThreadLocalMap将线程本地变量(ThreadLocal)作为key,线程变量的副本作为value。每个线程去使用共享变量时,实际调用threadLocal的get()方法,获取当前线程对应的ThreadLocalMap,然后在根据key获取value值,就实现了线程安全的操作变量副本的值了。

ThreadLocal源码解析

想要熟悉和理解 Threadlocal 的源码的话,我建议先思考这么三个问题:

1、 Threadlocal 为什么能实现每个线程能有一个独立的变量副本;

2、每个线程的变量副本的储存位置在哪儿;

3、变量副本是如何从共享变量中复制出来的;

首先我们来看 initialValue( ) 方法:返回的是本地线程变量的初始值。返回值为空的原因很简单,这个方法就是用来重写

* @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

②get()源码分析

2.1 get()源码入口

public T get() {
    //获取当前线程
         Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
         ThreadLocalMap map = getMap(t);
         if (map != null) {
             //如果ThreadLocalMap已经被创建了,那么通过当前的threadLocal对象作为key,获取value
             ThreadLocalMap.Entry e = map.getEntry(this);
             if (e != null) {
                 @SuppressWarnings("unchecked")
                 T result = (T)e.value;
                 return result;
            }
        }
    //如果ThreadLocalMap还没有被创建或者在ThreadLocalMap中查找不到此元素
         return setInitialValue();
    }

 2.1.1 ThreadLocalMap没初始化,ThreadLocalMap为null时,会调用setInitialValue()方法:


private T setInitialValue() {
    //initialValue方法一般会被重写,返回变量,不重写的话,直接返回null
         T value = initialValue();
         Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
         ThreadLocalMap map = getMap(t);
         if (map != null)
             //ThreadLocalMap已经被创建,那么直接设置初始值(即保存变量副本),初始值来自initialValue方法
             map.set(this, value);
         else
             //创建ThreadLocalMap
             createMap(t, value);
         return value;
    }

其中,initialValue()方法是由我们重写的,需要注意的是,返回值必须为new一个对象,而不是直接返回一个对象引用。因为如果多个线程都保存同一个引用的副本的话,那他们通过这个引用修改共享变量的值,是相互影响的。我们本来的目的便是为了获取共享变量的初始值副本,各个线程对副本的修改不影响变量本身。这就是能实现每个线程能有一个独立的变量副本原因

2.1.2 看看createMap是如何创建threadLocalMap的:

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

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
      //创建一个初始容量为16的Entry数组
             table = new Entry[INITIAL_CAPACITY];
    //通过threadLocal的threadLocalHashCode来定位在数组中的位置
             int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //保存在数组中
             table[i] = new Entry(firstKey, firstValue);
    //记录下已用的大小
             size = 1;
    //设置阈值为容量的2/3
             setThreshold(INITIAL_CAPACITY);
        } 

 2.2 初始化threadLocalMap之后,此线程再次调用get()方法,又做了哪些操作呢

private Entry getEntry(ThreadLocal<?> key) {
             int i = key.threadLocalHashCode & (table.length - 1);
             Entry e = table[i];
             if (e != null && e.get() == key)
                 return e;
             else
                 //如果定位的元素的key与传入的key不相等,那么一直往后找
                 return getEntryAfterMiss(key, i, e);
        }

 可以看到是通过map.getEntry(this)去查找元素的,返回Entry。

2.3 如果map.getEntry(this)也找不到元素怎么办?回顾前面讲的get入口,先判断是否能根据当前线程获取threadLocalMap,第一种情况:threadLocalMap为空,那么直接新初始化创建一个。第二请情况:threadLocalMap有值,但是map.getEntry(this) 为空,这个时候就会在初始化方法里调用map.set(this, value)方法,将当前参数设置进Map。

private T setInitialValue() {
    //initialValue方法一般会被重写,不重写的话,直接返回null
         T value = initialValue();
         Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
         ThreadLocalMap map = getMap(t);
         if (map != null)
             //ThreadLocalMap已经被创建,那么直接设置初始值(即保存变量副本),初始值来自initialValue方法
             map.set(this, value);
         else
             //创建ThreadLocalMap
             createMap(t, value);
         return value;
    }
private void set(ThreadLocal<?> key, Object value) {
             Entry[] tab = table;
             int len = tab.length;
             int i = key.threadLocalHashCode & (len-1);

             for (Entry e = tab[i];
                  e != null;
                  e = tab[i = nextIndex(i, len)]) {
                 ThreadLocal<?> k = e.get();

                 if (k == key) {
                     e.value = value;
                     return;
                }

                 if (k == null) {
                     //替代过期的元素,并清除后面的一些过期元素
                     replaceStaleEntry(key, value, i);
                     return;
                }
            }

    //如果在table中确实找不到,那么新建一个
             tab[i] = new Entry(key, value);
             int sz = ++size;
             if (!cleanSomeSlots(i, sz) && sz >= threshold)
                 //如果没有元素被清除,且超过阈值,那么扩容并重新hash定位
                 rehash();
        }

③set()源码分析

ThreadLocalMap的set方法和get方法很类似


public void set(T value) {
   //获取当前线程
   Thread t = Thread.currentThread();
   //获取当前线程的ThreadLocalMap
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}

底层源码工作原理总结

首先使用ThreadLocal<?>维护变量时,重写initialValue()方法,返回线程本地变量的初始值。然后每个线程去使用共享变量时,实际调用threadLocal的get()方法,获取当前线程对应的ThreadLocalMap。进入 get函数,先判断是否能根据当前线程获取threadLocalMap,第一种情况:threadLocalMap为空,那么直接新初始化创建一个。第二请情况:threadLocalMap有值,但是map.getEntry(this) 为空,这个时候就会在初始化方法里调用map.set(this, value)方法,将当前参数设置进Map。第三种情况:threadLocalMap有值,map.getEntry(this) 有值 根据key获取value直接返回,前两种情况返回初始化value,实现安全访问。

threadLocal的set()方法,作用:设置当前线程的线程局部变量的值,实现根据当前线程获取对应的threadLocalMap,获取到map直接将值set进去,获取map为空,直接创建threadLocalMap将值设置进去。

二、ThreadLocal的内存泄露分析

在分析ThreadLocal导致的内存泄露前,需要普及了解一下内存泄露、强引用与弱引用以及GC回收机制,这样才能更好的分析为什么ThreadLocal会导致内存泄露呢?更重要的是知道该如何避免这样情况发生,增强系统的健壮性。

内存泄露

内存泄露是程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光

通俗的讲:内存一直被对象或者变量占用,导致内存不能被回收

强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

GC回收机制-如何找到需要回收的对象

JVM如何找到需要回收的对象,方式有两种:

  • 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,

  • 可达性分析法:以根集对象为起始点进行搜索,如果有对象不可达的话,即没有引用指向的对象,就是垃圾对象,jvm垃圾回收的时候将会对垃圾对象进行回收。(根集一般包括Java栈中引用的对象,方法区常量池中引用的对象,堆中引用的对象等)

不可达定义:在java中,对象是通过引用使用的,如果在没有引用指向该对象的情况下,那么将无从处理或调用该对象,这样的对象为不可达。

ThreadLocal的内存泄露分析

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,线程一直没有完成,如果都没有手动删除对应key,都会导致内存泄漏。源码开发也考虑到了这一点

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

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

但这次发现:Entry是继承的WeakReference,并且只绑定了ThreadLocal(WeakReference表示弱引用对象)。

使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,但是value就不同了,它是强引用,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

防止内存泄漏最直接的方法就是使用完变量后调用ThreadLocal的remove(),remove()实际是将对象的引用置为null,这样一来没有引用指向这个对象,该对象就会被JVM判定为垃圾并在GC时回收掉。

三、ThreadLocal在set()时发生哈希冲突怎么办吗

数据是以键值对方式存进Entry数组的,在存入时会根据键(ThreadLocal)的哈希值,找到它所存放的位置,但这样有时会出现哈希冲突,至于如何应对哈希冲突

  1. 如果该位置是空的,那么直接将键值对存储;

  2. 若不为空且两个键相同,那么新值换旧值;

  3. 若不为空且两键不相同,那只能找下个空位置了。

文章参考:

https://mp.weixin.qq.com/s/I3hLAcA_cbBG25MGfqKFuw

https://mp.weixin.qq.com/s/B2-sZ9rO8xyrag2j-Xaqlg

https://mp.weixin.qq.com/s/BFNIDxWGJJy_NkIdi9Z7uQ

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章