【Java多线程】ThreadLocal 原理分析与使用场景

一.线程隔离

当多线程访问时,通过将数据封闭在各自的线程中相互隔离,互不干扰的技术称为线程隔离ThreadLocal就是线程隔离的一种体现

二.ThreadLocal是什么

  • ThreadLocal类提供了一种线程局部变量(ThreadLocal),即每一个线程都会保存一份变量副本,每个线程都可以独立地修改自己的变量副本,而不会影响到其他线程
  • ThreadLocal 变量通常被private static修饰,其中保存变量属于当前线程,该变量对其他线程而言是隔离的,当一个线程结束时,它所使用的所有 ThreadLocal 实例副本都可被回收。
  • ThreadLocal 适用于变量在线程间隔离而在方法或类间共享的场景。
  • ThreadLocal唯一的缺点就是:只能用于存储当前线程的变量。子线程获取不到父线程的数据(使用InheritableThreadLocals`可以解决)

三.ThreadLocal类提供的方法

方法 描述
public T get() 获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) 设置当前线程中变量的副本
public void remove() 移除当前线程中变量的副本
protected T initialValue() initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法, 返回此线程局部变量当前副本中的初始值

ThreadLocalMap是ThreadLocal的静态内部类,该类才是实现线程隔离机制的关键。,get()、set()、remove()方法底层都是对该内部类进行操作,ThreadLocalMap用键值对方式存储每个线程变量的副本,key当前ThreadLocal对象value对应线程的变量副本

四.入门使用

假设每个线程都需要一个计数器记录自己做某件事做了多少次,各线程运行时都需要改变自己的计数值而且相互不影响,那么ThreadLocal就是很好的选择,这里ThreadLocal里保存的当前线程的局部变量的副本就是这个计数值。

public class SeqCount {

  private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
      @Override
      protected Integer initialValue() {//初始化值为0
          return 0;
      }
  };

    //当前统计 + 1,然后返回
    public int nextSeq() {
        seqCount.set(seqCount.get() +1);
        return seqCount.get();
    }

    public static void main(String [] args) {
        SeqCount seqCount = new SeqCount();

        //开启四个线程
        SeqThread seqThread1 = new SeqThread(seqCount);
        SeqThread seqThread2 = new SeqThread(seqCount);
        SeqThread seqThread3 = new SeqThread(seqCount);
        SeqThread seqThread4 = new SeqThread(seqCount);

        seqThread1.start();
        seqThread2.start();
        seqThread3.start();
        seqThread4.start();
    }
    
    //静态内部类,循环调用3次seqCount.nextSeq()方法
    public static class SeqThread extends Thread {

        private SeqCount seqCount;

        public SeqThread(SeqCount seqCount) {
            this.seqCount = seqCount;
        }

        @Override
        public void run() {
            for (int i=0; i<3; i++) {
                System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq());
            }
        }
    }
}

执行结果:
在这里插入图片描述

五.ThreadLocal

1.ThreadLocal的数据结构

在这里插入图片描述

  1. Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。

  2. ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

  3. 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的ThreadLocalMap里找对应的key,从而实现了线程隔离。

  4. ThreadLocalMap类似于HashMap的结构,只是·HashMap是由数组+链表(数组组+链表+红黑树结构,当链表长度大于8,转为红黑树)实现的,而ThreadLocalMap中并没有链表结构,使用Entry来保存键值对, 它的key是ThreadLocal<?> k,继承自WeakReference, 也就是我们常说的弱引用类型,在发生GC时会被回收。

    扫描二维码关注公众号,回复: 11382388 查看本文章

1.1.Java的四种引用类型

Java有四种引用类型,引用强度从强到弱依次为:强引用、软引用、弱引用和虚引用

  • 强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候

  • 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收

  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收

  • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

1.2.GC之后,Entry的key是否是null?

ThreadLocal 是弱引用,那么在threadLocal.get()的时候,发生GC之后,key是否是null?

我们使用反射的方式来看看GC当前线程的ThreadLocalMap的数据情况:

public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {
        //开启线程1并调用test()方法
        Thread test1 = new Thread(() -> test("11111", false),"test1");
        test1.start();
        //当前线程t等待主线程执行完在执行
        test1.join();

        System.out.println("--gc后--");

        //开启线程1并调用test()方法且调用GC()
        Thread test2 = new Thread(() -> test("22222", true),"test2");
        test2.start();
        //当前线程t2等待主线程执行完在执行
        test2.join();
    }

	 /**
     * 测试GC和非GC时当前线程的ThreadLocal是否被回收
     * @param s 字符串
     * @param isGC 是否调用GC true调用/false不调用
     */
    private static void test(String s, boolean isGC) {
        try {
            //当前线程设置ThreadLocal
            new ThreadLocal<>().set(s);
            
            //发起GC
            if (isGC) {
                System.gc();
            }
            // 获取当前线程
            Thread currentThread = Thread.currentThread();
            // 获取当前线程的class
            Class<? extends Thread> clazz = currentThread.getClass();
            // 获取当前线程的threadLocals属性对象
            Field field = clazz.getDeclaredField("threadLocals");
            // 设置threadLocals字段的可见级别
            field.setAccessible(true);
            //获取当前线程对象的threadLocals的属性值
            Object threadLocalMap = field.get(currentThread);

            // 获取当前线程 threadLocals属性的class(ThreadLocal.ThreadLocalMap)
            Class<?> tlmClass = threadLocalMap.getClass();
            // 获取当前线程ThreadLocal.ThreadLocalMap类内的table属性对象
            Field tableField = tlmClass.getDeclaredField("table");
            // 设置ThreadLocal.ThreadLocalMap类内table字段的可见级别
            tableField.setAccessible(true);
            // 获取当前线程的ThreadLocal.ThreadLocalMap类的 table字段的属性值 (Entry[] table)
            Object[] entryArr = (Object[]) tableField.get(threadLocalMap);

            //遍历ThreadLocal.ThreadLocalMap.table属性 (Entry[] table)
            for (Object entry : entryArr) {
                if (entry != null) {
                    //获取当前entry的class
                    Class<?> entryClass = entry.getClass();

                    //获取当前entry的value字段并设置字段可见性
                    Field valueField = entryClass.getDeclaredField("value");
                    valueField.setAccessible(true);

                    //获取当前entry的key字段并设置字段可见性
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    referenceField.setAccessible(true);

                    //打印key/value
                    System.out.println(String.format("ThreadName=[%s],弱引用key=[%s],值=[%s]", currentThread.getName(),referenceField.get(entry), valueField.get(entry)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果:
在这里插入图片描述
test1线程Debug详情
在这里插入图片描述

test2线程Debug详情
在这里插入图片描述

结论:ThreadLocal是弱引用,在发生GC时会自动会回收掉,但如果ThreadLocal对应的value是强引用则不会被回收,也就是会出现我们 value 没被回收,key 被回收,导致 value 永远存在,出现内存泄漏问题。

1.3.ThreadLocal重要属性

// 当前 ThreadLocal 的 hashCode,由 nextHashCode() 计算而来,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();

// 哈希魔数,主要与斐波那契散列法以及黄金分割有关
private static final int HASH_INCREMENT = 0x61c88647;

// 保证了在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是唯一的
private static AtomicInteger nextHashCode = new AtomicInteger();
/*
* threadLocalHashCode`是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了
* 生成过程则是调用`nextHashCode():`
*/

// 返回计算出的下一个哈希值,其值为 i * HASH_INCREMENT,其中 i 代表调用次数
private static int nextHashCode() {
	//该函数简单地通过一个增量HASH_INCREMENT来生成hashcode。
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,
//HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCod的增量。

/*
至于为什么这个增量为0x61c88647?
	主要是因为ThreadLocalMap的初始大小为16,每次扩容都会为原来的2倍,这样它的容量永远为2的n次方,
	该增量选为0x61c88647也是为了尽可能均匀地分布,减少碰撞冲突。
*/

其中的 HASH_INCREMENT也不是随便取值的

  • 转换为十进制是 1640531527,2654435769
  • 转换成 int 类型就是-1640531527,2654435769
  • 等于(√5-1)/2 乘以 2 的 32 次方(√5-1)/2就是黄金分割数,近似为0.618,也就是说 0x61c88647可以理解为一个黄金分割数乘以 2 的 32 次方,它可以保证nextHashCode 生成的哈希值,均匀的分布在2 的幂次方上,且小于 2 的 32 次方

示例代码:

private static final int HASH_INCREMENT = 0x61c88647;

public static void main(String[] args) throws Exception {
    int n = 5;
    int max = 2 << (n - 1);
    for (int i = 0; i < max; i++) {
        System.out.print(i * HASH_INCREMENT & (max - 1));
        System.out.print(" ");

    }
}

运行结果为:0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25
.
可以发现元素索引值完美的散列在数组当中,并没有出现冲突。

2.ThreadLocal.ThreadLocalMap

  • 因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
  • ThreadLocalMap使用用Entry类来进行存储,key为当前ThreadLocal对象的引用,value为我们要存储的值,
    -我们使用的 ThreadLocal.get()、ThreadLocal.set(),ThreadLocal.remove()方法其实都是 底层先获取这个这个ThreadLocalMap,然后调用这个map对应的 get()、set() ,remove()来实现增删改查。

源码如下:

static class ThreadLocalMap {
	/**
	 * 键值对实体的存储结构
	 * Entry继承WeakReference,所以Entry对应key的引用(ThreadLocal实例)是一个弱引用。)
	 */
	static class Entry extends WeakReference<ThreadLocal<?>> {
		// 当前线程关联的 value,这个 value 并没有用弱引用追踪
		Object value;

		/**
		 * 构造键值对
		 *
		 * @param k k 为 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
		 * @param v v 为 value
		 */
		Entry(ThreadLocal<?> k, Object v) {
			super(k);
			value = v;
		}
	}

	// 初始容量,必须为 2 的幂
	private static final int INITIAL_CAPACITY = 16;

	// 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
	private Entry[] table;

	// ThreadLocalMap 元素数量
	private int size = 0;

	// 扩容的阈值,默认是数组大小的三分之二(1.5倍)
	private int threshold;

   //----------------省略其他代码-------------
}

源码中发现 ThreadLocalMap 就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小数组的元素是 EntryEntry 的 key 就是 ThreadLocal 的引用value 是 ThreadLocal 的值。ThreadLocalMap 解决 hash 冲突的方式采用的是 线性探测法如果发生冲突会继续寻找下一个空的位置。

2.1.ThreadLocalMap.set()解析

	/**
	 * key 当前threadLocal的引用
	 * value 要存储的值
	 */
private void set(ThreadLocal<?> key, Object value) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    //获取Entry数组长度
    int len = tab.length;
    // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置(计算 key 在数组中的下标)
    int i = key.threadLocalHashCode & (len-1);
    // 采用“线性探测法”,寻找合适位置(索引处为空即是合适的位置)(如果发生冲突会继续寻找下一个空的位置)
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i] ; e != null; e = tab[i = nextIndex(i, len)]) {
        //获取该哈希值处的ThreadLocal对象
        ThreadLocal<?> k = e.get();
        // key 存在,直接覆盖
        if (k == key) {
            e.value = value;
            return;
        }
        // key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
        if (k == null) {
            // 用新元素替换陈旧的元素
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    int sz = ++size;
    // cleanSomeSlots 清楚陈旧的Entry(key == null)
    // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值(数组大小的三分之二),则进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
    // 扩容的过程也是对所有的 key 重新哈希的过程
        rehash();
}


/**
* 索引位置 + 1
*/
private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
}
  • ThreadLocalMap.set()方法和Map.put()方法差不多,但是有一点区别是:Map.put方法处理哈希冲突使用的是 链地址法,而set方法使用的 开放地址法

  • ThreadLocalMap.set()中的replaceStaleEntry()cleanSomeSlots(),这两个方法可以清除掉key ==null的实例,防止内存泄漏

2.2.ThreadLocalMap.getEntry()解析

/**
* 当前ThreadLocal的引用
*/
private Entry getEntry(ThreadLocal<?> key) {
	//// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置(计算 key 在数组中的下标)
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

由于采用了开放定址法,所以当前key的散列值和元素在数组中的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

这里有一个重要的地方,当key==null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。

3.ThreadLocal 的 get()方法解析

public T get() {
	// 返回当前 ThreadLocal 所在的线程
	Thread t = Thread.currentThread();
	// 从线程中拿到 ThreadLocalMap
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// 从 ThreadLocalMap 中拿到 entry
		ThreadLocalMap.Entry e = map.getEntry(this);
		// 如果不为空,读取当前 ThreadLocal 中保存的值
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T) e.value;
			return result;
		}
	}
	// 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,最后返回当前的 ThreadLocal 对象关联的初值,即 value
	return setInitialValue();
}

//初始化当前线程的 ThreadLocal
private T setInitialValue() {
        T value = initialValue();//initialValue不重写默认返回null
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
}

//getMap()方法可以获取当前线程所对应的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//初始化ThreadLocal方法
protected T initialValue() {
        return null;
}

//初始化当前线程的ThreadLocalMap
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}

//ThreadLocalMap的构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
			//初始化Entry数组容量
            table = new Entry[INITIAL_CAPACITY];
            //初始化第一个key的索引
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //第一个Entry的存储
            table[i] = new Entry(firstKey, firstValue);
            //长度为1
            size = 1;
            //设置ThreadLocalMap的长度
            setThreshold(INITIAL_CAPACITY);
}

//设置ThreadLocalMap的长度
private void setThreshold(int len) {
            threshold = len * 2 / 3;
}

get 方法的主要流程为:

  1. 先获取到当前线程的引用
  2. 获取当前线程内部ThreadLocalMap
  3. 如果 ThreadLocalMap 存在,通过ThreadLocalMap的getEntry()方法 获取当前 ThreadLocal 对应的 value 值
  4. 如果 ThreadLocalMap 不存在或者找不到 value 值,则调用 setInitialValue() 进行初始化

get 方法的时序图如下所示:
在这里插入图片描述
其中每个 Thread 的ThreadLocalMapthreadLocal 作为key,保存自己线程的 value 副本,是保存在每个线程中,并没有保存在 ThreadLocal 对象中。

其中 ThreadLocalMap.getEntry() 方法的源码如下:

/**
 * 返回 key 关联的键值对实体
 *
 * @param key threadLocal
 * @return
 */
private Entry getEntry(ThreadLocal<?> key) {
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
	// 若 e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回
	if (e != null && e.get() == key) {
		return e;
	} else {
		// 从 i 开始向后遍历找到键值对实体
		return getEntryAfterMiss(key, i, e);
	}
}

ThreadLocalMap 的 resize 方法
当 ThreadLocalMap 中的 ThreadLocal 的个数超过容量阈值时,ThreadLocalMap 就要开始扩容了,我们一起来看下 resize 的源代码:

/**
 * 扩容,重新计算索引,标记垃圾值,方便 GC 回收
 */
private void resize() {
	Entry[] oldTab = table;
	int oldLen = oldTab.length;
	int newLen = oldLen * 2;
	// 新建一个数组,按照2倍长度扩容
	Entry[] newTab = new Entry[newLen];
	int count = 0;

	// 将旧数组的值拷贝到新数组上
	for (int j = 0; j < oldLen; ++j) {
		Entry e = oldTab[j];
		if (e != null) {
			ThreadLocal<?> k = e.get();
			// 若有垃圾值,则标记清理该元素的引用,以便GC回收
			if (k == null) {
				e.value = null;
			} else {
				// 计算 ThreadLocal 在新数组中的位置
				int h = k.threadLocalHashCode & (newLen - 1);
				// 如果发生冲突,使用线性探测往后寻找合适的位置
				while (newTab[h] != null) {
					h = nextIndex(h, newLen);
				}
				newTab[h] = e;
				count++;
			}
		}
	}
	// 设置新的扩容阈值,为数组长度的三分之二
	setThreshold(newLen);
	size = count;
	table = newTab;
}

resize 方法主要是进行扩容,同时会将垃圾值标记方便 GC 回收,扩容后数组大小是原来数组的两倍。

4. ThreadLocal 的initialValue()解析

protected T initialValue() {
    return null;
}

在上面的代码分析get()的过程中,我们发现如果没有先set的话,即在ThreadLocalMap中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null

该方法定义为protected级别且返回为null,所以我们在使用ThreadLocal的时候一般都应该重写该方法。

注意:如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

5.ThreadLocal 的 set()解析

public void set(T value) {
	// 返回当前ThreadLocal所在的线程
	Thread t = Thread.currentThread();
	// 返回当前线程持有的ThreadLocalMap 
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
		map.set(this, value);
	} else {
		// 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
		createMap(t, value);
	}
}

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

set 方法的作用是把我们想要存储的 value 给保存进去。set 方法的流程主要是:

  1. 先获取到当前线程的引用
  2. 利用这个引用来获取到 ThreadLocalMap
  3. 如果 ThreadLocalMap 为空,则去创建一个 ThreadLocalMap
  4. 如果 ThreadLocalMap 不为空,就利用 ThreadLocalMap 的 set 方法将 value 添加到 map 中

通过createMap可以看出最终的变量是放在了当前线程的 ThreadLocalMap中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装传递了变量值


set 方法的时序图如下所示:
在这里插入图片描述
其中map就是我们上面讲到的ThreadLocalMap,可以看到它是通过当前线程对象获取到的 ThreadLocalMap,接下来我们看 getMap方法的源代码:

/**
 * 返回当前线程 thread 持有的 ThreadLocalMap
 *
 * @param t 当前线程
 * @return ThreadLocalMap
 */
ThreadLocalMap getMap(Thread t) {
	return t.threadLocals;
}

getMap 方法的作用主要是获取当前线程内的 ThreadLocalMap 对象可以看出,原来 threadLocals 是线程的一个属性所以在多线程环境下 threadLocals 是线程安全的,下面让我们看看 Thread 类中的相关代码:
在这里插入图片描述
可以看出每个线程都有 ThreadLocalMap 对象,被命名为 threadLocals,默认为 null,所以每个线程的 ThreadLocals 都是隔离独享的。

调用 ThreadLocalMap.set() 时,会把当前 threadLocal 对象作为key,想要保存的对象作为value,存入 map。
ThreadLocalMap.set() 的源码如下

/**
 * 在 map 中存储键值对<key, value>
 *
 * @param key   threadLocal
 * @param value 要设置的 value 值
 */
private void set(ThreadLocal<?> key, Object value) {
	Entry[] tab = table;
	int len = tab.length;
	// 计算 key 在数组中的下标
	int i = key.threadLocalHashCode & (len - 1);
	// 遍历一段连续的元素,以查找匹配的 ThreadLocal 对象
	for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
		// 获取该哈希值处的ThreadLocal对象
		ThreadLocal<?> k = e.get();

		// 键值ThreadLocal匹配,直接更改map中的value
		if (k == key) {
			e.value = value;
			return;
		}

		// 若 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}

	// 直到遇见了空槽也没找到匹配的ThreadLocal对象,那么在此空槽处安排ThreadLocal对象和缓存的value
	tab[i] = new Entry(key, value);
	int sz = ++size;
	// 如果没有元素被清理,那么就要检查当前元素数量是否超过了容量阙值(数组大小的三分之二),以便决定是否扩容
	if (!cleanSomeSlots(i, sz) && sz >= threshold) {
		// 扩容的过程也是对所有的 key 重新哈希的过程
		rehash();
	}
}

Thread、ThreadLocal 以及 ThreadLocalMap 的关系
在这里插入图片描述
从上面又可以看出,ThreadLocalMap是在ThreadLocal中使用内部类来编写的,但对象的引用是在Thread中!

于是我们可以总结出:Thread为每个线程维护了ThreadLocalMap这么一个Map(类型是ThreadLocal.ThreadLocalMap,也就是说每个线程有一个自己的ThreadLocalMap ),而ThreadLocalMap保存的EntrykeyThreadLocal对象本身value则是要存储的对象

一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal!!!
在这里插入图片描述

5.remove()

public void remove() {
	// 返回当前线程持有的 ThreadLocalMap 
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null) {
		// 从 ThreadLocalMap 中清理当前 ThreadLocal 对象关联的键值对
		m.remove(this);
	}
}

remove 方法的时序图如下所示:
在这里插入图片描述

  1. 根据当前线程的引用获取到对应的ThreadLocalMap
  2. 如果ThreadLocalMap不为空,调用它的remove方法,从 ThreadLocalMap 中清理当前 ThreadLocal 对象关联的键值

六.ThreadLocal 内存泄漏

  1. ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次GC时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。

    1. 弱引用即WeakReference,表示如果弱引用的指向的对象只存在弱引用这一条线路,则下次YGC时会被回收。
    2. 当仅仅只有ThreadLocalMap中的Entry的key指向ThreadLocal的时候,ThreadLocal会进行回收的!!!
  2. 因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

  3. JVM团队已经考虑到这样的情况,并采取一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

    如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

七.ThreadLocal 应用场景

ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:

  • 方便同一个线程使用某一对象,避免不必要的参数传递;
  • 线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
  • 获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);
  • Spring 事务管理器采用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
  1. 每个线程需要有自己单独的实例
  2. 实例需要在多个类/方法中共享,但不希望被多线程共享
    • 对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
    • 对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。扩展:

1)存储用户Session

private static final ThreadLocal threadSession = new ThreadLocal();

    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

2)解决线程安全的问题
比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

这里的DateUtil.formatDate()就是线程安全的了。

  • Java8里的 java.time.format.DateTimeFormatter是线程安全的
  • Joda time里的DateTimeFormat也是线程安全的

这类场景阿里规范里面也提到了
在这里插入图片描述

八.可继承的ThreadLocal-InheritableThreadLocal

ThreadLocal只能用于存储当前线程的变量。子类线程获取不到父类线程的数据。inheritableThreadLocals就是用来解决父子线程独立变量共享问题。

如果我在主线程中set一个值,这个时候我在新创建的线程中是读取不到的,因为Threadlocal不支持继承性。

static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
/**
*测试在主线程中创建子线程,然后获取ThreadLocal的值
*/
 @Test
public  void testMainCreateChildThread1() {
    threadLocal.set(1000);
    new Thread(() -> {
         System.out.println(Thread.currentThread()+"------"+threadLocal.get());
     }).start();
}

输出结果:
Thread[Thread-0,5,main]------null

也就是说Threadlocal不支持继承性主线程设置了值,在子线程中是获取不到的。那我现在想要获取主线程里面的值要怎么做?

Threadlocal有一个子类InheritableThreadLocal 专门用来解决父子线程独立变量共享问题。

static ThreadLocal<Integer> integerInheritableThreadLocal = new InheritableThreadLocal<>();
/**
*测试在主线程中创建子线程,然后获取InheritableThreadLocal的值
*/
@Test
public  void testMainCreateChildThread2() {
     integerInheritableThreadLocal.set(2000);
     new Thread(() -> {
        System.out.println(Thread.currentThread().getName()+"------"+integerInheritableThreadLocal.get());
     }).start();
}

输出结果:
Thread[Thread-0,5,main]====2000

运行结果发现子线程是可以获取到主线程设置的值的,那它是如何实现的?

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

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

InheritableThreadLocal继承Threadlocal的,并且把threadlocals给替换成inheritableThreadLocal么替换成inheritableThreadLocals`后子线程就可以获取到主线程设置的属性了吗?我们在看一下Thread类中init的的实现。

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
		//获取主线程
        Thread parent = currentThread();
       
        //-------省略无关代码--------------
        //-------省略无关代码--------------
        
		/*
		*inheritThreadLocals 设置为true并且父类线程inheritableThreadLocals有共享数据则
		*创建一个父类线程inheritableThreadLocals副本,然后复制给当前线程的inheritableThreadLocals变量来实现父子线程共享
		*/
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //创建一个父类线程inheritableThreadLocals副本并设置到当前线程的inheritableThreadLocals中
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

Threadinit()方法可以看出,先获取了当前线程(主线程)判断当前线程父线程的inheritableThreadLocals不为空的话就调用ThreadLocal.createInheritedMap方法赋值给子线程中的inheritableThreadLocals。
在这里插入图片描述
InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。

当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个TransmittableThreadLocal组件就可以解决这个问题,这里就不再延伸,感兴趣的可自行查阅资料。

九.为什么建议使用static修饰ThreadLocal?

  1. 首先static修饰的变量是在类加载时就分配好内存空间,在类卸载才会被回收,这一点请明确.
    2.ThreadLocal是 ThreadLocalMap 中Entry 的 key,而用 static 修饰 ThreadLocal,保证了 ThreadLocal 有强引用在,也就是 Entry 的 key有被强引用指向,会一直存在,垃圾回收的时候不会被回收
  2. ThreadLocal的原理是在Thread内部有一个ThreadLocalMap的集合对象,他的key是ThreadLocal,value就是你要存储的变量副本, 不同的线程的ThreadLocalMap是相互隔离的,如果变量ThreadLocal是非static的就会造成每次生成实例都要生成不同的ThreadLocal对象,虽然这样程序不会有什么异常,但是会浪费内存资源.造成内存泄漏.

十.ThreadLocal的注意事项

1.ThrealLocal脏数据和内存泄漏问题

入坑:

  1. 脏数据问题:线程复用导致产生脏数据。由于线程池会复用Thread对象,进而Thread对象中的threalLocals也会被复用,导致Thread对象在执行其他任务时通过get()方法获取到之前任务设置的数据,从而产生脏数据。
  2. 内存泄漏问题:ThreadLocal通常是使用static关键字修饰的。如果开发人员单纯寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的Value,那么就会导致内存泄漏,Entry的Value无法被回收。

脱坑:

  1. 解决脏数据:线程执行前重新调用set()设置值。线程复用导致产生脏数据,如果复用线程在执行下个任务之前调用set()重新设置值,那么脏数据问题就不会出现了。
  2. 解决内存泄漏:线程执行完后调用remove()完成收尾工作。无法依托弱引用机制来回收Entry的Value,那就调用ThreadLocal的remove方法显式清除。

最后,Entry的弱引用机制不是导致ThreadLocal内存泄漏的原因,它的存在只是增加了开发人员的理解难度,就算没有弱引用机制,线程执行完不调用remove()清除也会存在内存泄漏问题。

2.ThreadLocal结合线程池的问题

当 ThreadLocal 配合线程池使用的时候,我们需要及时对 ThreadLocal 进行清理,清除与本线程绑定的 value 值,否则会出现意料之外的结果。

来看看没有调用remove方法和有调用remove下的结果差异。

private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 5; i++) {
        executorService.execute(()->{
            Integer before = threadLocal.get();
            threadLocal.set(before + 1);
            Integer after = threadLocal.get();
            System.out.println("before: " + before + ",after: " + after);
        });
    }
    executorService.shutdown();
}

没有调用 remove 方法进行清理

before: 0,after: 1
before: 0,after: 1
before: 1,after: 2
before: 2,after: 3
before: 3,after: 4

可以看到出现了 before 不为0的情况,这是因为线程在执行完任务被复用了,被复用的线程使用了上一个线程操作的value对象,从而导致不符合预期。

加上调用remove方法的逻辑:

try {
    Integer before = threadLocal.get();
    threadLocal.set(before + 1);
    Integer after = threadLocal.get();
    System.out.println("before: " + before + ",after: " + after);
} finally {
    threadLocal.remove();
}
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1
before: 0,after: 1

3. 线程池异步调用,requestId传递

因为 org.slf4j.MDC 是基于ThreadLocal去实现的,异步过程中,子线程并没有办法获取到父线程ThreadLocal存储的数据,所以这里可以自定义线程池执行器,修改其中的run()方法:

public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    @Override
    public void execute(Runnable runnable) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        super.execute(() -> run(runnable, context));
    }

    @Override
    private void run(Runnable runnable, Map<String, String> context) {
        if (context != null) {
            MDC.setContextMap(context);
        }
        try {
            runnable.run();
        } finally {
            MDC.remove();
        }
    }
}

十一.ThreadLocal原理总结

  1. ThreadLocal是用来提供线程局部变量的,在线程内可以随时随地的存取数据,而且线程之间是互不干扰的。

  2. ThreadLocal实际上是在每个线程内部维护了一个ThreadLocalMap,这个ThreadLocalMap是每个线程独有的,里面存储的是Entry对象,Entry对象实际上是个ThreadLocal的实例的弱引用,同时还保存了value值,也就是说Entry存储的是键值对key就是ThreadLocal实例引用,value则是要存储的数据

  3. TreadLocal的核心是底层维护的ThreadLocalMap,它的底层是一个自定义的哈希表增长因子是2/3,增长因子也可以叫做是一个阈值,底层定义为threshold,当哈希表容量大于或等于阈值的3/4的时候就开始扩容底层的哈希表数组table

  4. ThreaLocalMap中存储的核心元素是Entry,Entry是一个弱引用,所以在GC的时候,ThreadLocal如果没有外部的强引用,它会被回收掉,这样就会产生key为null的Entry了,这样也就产生了内存泄漏

  5. ThreadLocalget(), set()remove()的时候都会清除ThreadLocalMap中key为null的Entry,如果我们不手动清除,就会造成内存泄漏,最佳做法是使用ThreadLocal就像使用锁一样,加锁之后要解锁,也就是用完就使用remove进行清理。

xxxxxx

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/106404474