Java中的线程本地变量ThreadLocal

Posted 攻城狮Chova

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的线程本地变量ThreadLocal相关的知识,希望对你有一定的参考价值。

基本概念

  • 在处理多线程中最常用的方法就是使用锁,通过锁来控制多个不同的线程对临界区的访问,但是锁会在并发冲突时对性能造成影响,这是可以使用线程本地变量ThreadLocal避免锁竞争导致的并发冲突
    • ThreadLocal的变量只有当前自身线程可以访问,其余线程无法访问,这样可以避免线程竞争
    • ThreadLocal提供的线程安全方式不是在发生并发线程冲突时解决冲突,而是彻底避免的线程冲突的发生

ThreadLocal的原理

get

public T get() {
	// 获取当前线程
	Thread t = Thread.currentThread();
	/*
	 * 每个线程自身都有一个ThreadLocalMap
	 * ThreadLocalMap中保存着所有的ThreadLocal变量
	 */
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		/*
		 * ThreadLocalMap的key就是当前ThreadLocal对象的实例
		 * 多个ThreadLocal变量都保存在这个ThreadLocalMap中
		 */
		ThreadLocal.Entry e = map.getEntry(this);
		if (e != null) {
			// ThreadLocalMap中取出的值就是本地变量ThreadLocal变量
			@SuppressWarnings("unchecked")
			T result = e.value;
			return result;
		}
	}
	// 如果ThreadLocalMap没有进行初始化,就在这里对ThreadLocalMap进行初始化
	return setInitialValue();
}

ThreadLocalMap

  • ThreadLocal变量保存在每个线程的map中,这个map就是Thread对象中的threadLocals字段
ThreadLocal.ThreadLocalMap threadLocals = null;
  • Thread.ThreadLocalMap是一个特殊的Map, 其中每个Entrykey都是一个弱引用:
    • 当这个变量不再被其余的对象使用时,可以自动回收这个ThreadLocal对象,避免可能存在的内存泄漏问题
    • 注意: 此时Entry中的key依旧是一个强引用,仍然可能存在如何回收,内存泄漏的问题
static class Entry extends WeakReference<ThreadLocal<?>> {
	/**
	 * The value associated with
	 */
	Object value;

	// 弱引用的key
	Entry(ThreadLocal<?> k, Object v) {
		super(k);
		value = v;
	}
}

ThreadLocal的使用

创建

private ThreadLocal<Integer> local = new ThreadLocal<>();
  • ThreadLocal是一个泛型类,需要指定ThreadLocal的变量类型

赋值

local.set(6)

获取

local.get()

初始化

  • ThreadLocal本地变量赋值时只有当前线程可见,所以无法通过其余线程进行赋值初始化
  • 如果需要统一初始化所有线程的ThreadLocal变量的值,需要使用ThreadLocal类中的withInitial() 方法,此时ThreadLocal变量的值是对所有线程是可见的
private ThreadLocal<Integer> local = ThreadLocal.withInitial(() -> 6);

ThreadLocal的问题

内存泄漏

  • ThreadLocalThreadLocalMap中的Entrykey是弱引用,当不存在外部强引用时 ,key就会被垃圾回收器自动回收
  • ThreadLocalThreadLocalMap中的Entryvalue依旧是强引用,这个value的引用过程如下:

Thread -> ThreadLocalMap -> Entry -> value

  • value的引用过程中可以看出,只有当线程Thread被回收时 ,ThreadLocalThreadLocalMap才可能被回收. 只要线程不退出 ,value总是会存在一个强引用
  • 对于线程池来说,大部分线程会一直存在于系统的整个生命周期中,这样会导致value始终都会存在一个强引用,造成value对象存在内存泄漏的可能
  • 解决:
    • ThreadLocalMap进行set(),get()remove() 操作的时候,对value值进行清理
    	private Entry getEntry(ThreadLocal<?> key) {
              int i = key.threadLocalHashCode & (table.length - 1);
              Entry e = table[i];
              if (e != null && e.get() == key)
              	  // 如果获取到key,直接返回Entry 
                  return e;
              else
              	  // 如果未获取到key,就尝试清理.如果访问的key总是存在,就不会进入这个方法
                  return getEntryAfterMiss(key, i, e);
          }
    
    
      	private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
              Entry[] tab = table;
              int len = tab.length;
    
              while (e != null) {
              	// Entry是一个弱引用
                  ThreadLocal<?> k = e.get();
                  if (k == key)
                  	// 如果获取到key,就返回Entry
                      return e;
                  if (k == null)
                  	// 如果key值为null,说明弱引用已经被回收.这时对value值进行清理回收
                      expungeStaleEntry(i);
                  else
                  	// 如果key值不是想要找的key,说明存在Hash冲突.在这里对冲突进行处理,寻找下一个Entry
                      i = nextIndex(i, len);
                  e = tab[i];
              }
              return null;
          }
    
    • 这里可以看出,对value值进行清理回收的方法为expungeStaleEntry() 方法.在remove()set() 方法中,都会直接或者间接调用这个回收方法对value值进行清理
  • 总结:
    • ThreadLocal中,为了避免内存泄漏,不仅使用了弱引用对key进行维护,还会在每个操作上检查key是否被回收,对value值进行清理回收
    • 因为ThreadLocal中的get() 方法总是会访问固定的几个一直存在的ThreadLocal, 这样会导致对value的清理回收动作一直不会执行.如果不手动调用set() 或者remove() 方法,依旧会存在内存泄漏的问题
    • 所以当不需要某个ThreadLocal变量时,应该主动调用remove() 方法,避免产生内存泄漏问题

Hash冲突

  • HashMapThreadLocalMapHash冲突处理的比较:
    • HashMap:
      • 使用链表法解决Hash冲突
      • 冲突中的每一个槽值中都一个链表,冲突的元素构成一个链表,放置在同一个槽位中
    • ThreadLocalMap:
      • 使用线性探测法解决Hash冲突
      • 如果放置的元素和槽值存在冲突,就选择下一个槽位存放
	private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            // 获取数组中Hash的一个位置
            int i = key.threadLocalHashCode & (len-1);
			/*
			 * 如果e == null,说明这个位置没有被占用,也就是没有冲突,不需要处理冲突,就不会进入循环
			 * 可以直接使用这个位置
			 * 如果e != null,说明发生冲突,就要进入循环中处理冲突,一直向下找,直到找到一个没有被占用的位置
			 */
            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;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocal的继承

InheritableThreadLocal

  • 有些数据需要进行父子线程之间的传递,比如存在ThreadLocal的父线程开立一个子线程,需要在子线程中访问主线程中的ThreadLocal对象
  • 因为在子线程中没有ThreadLocal对象,但是希望子线程可以获取到父线程的ThreadLocal对象,这里就可以使用InheritableThreadLocal对象替代ThreadLocal对象
  • 使用InheritableThreadLocal后,子线程就可以访问父线程中的ThreadLocal对象,但是要注意以下两点:
    • 变量的传递是发生在线程创建过程中,如果不是创建线程,而是使用线程池中的线程,子线程仍然无法获取到父线程中的ThreadLocal对象
    • 变量的传递就是从父线程的map中复制给子线程,两个线程的value是同一个对象.如果这个value对象本身是线程不安全的,那么两个线程中的value同样也是线程不安全的

以上是关于Java中的线程本地变量ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章

Java并发机制--ThreadLocal线程本地变量(转)

Java并发多线程编程——ThreadLocal(线程本地存储)

java中的ThreadLocal详解及示例代码

Java多线程 —— 深入剖析ThreadLocal

ThreadLocal 概念:run() 或 call() 中的任何变量都不是本地线程吗? [复制]

谈谈Java中的ThreadLocal