面试必问之ThreadLocal

彻底理解ThreadLocal

  • 场景
    在做日志分析时,我们想对一次request请求进行链路追踪,如何实现?

  • 方案
    首先定义一个标识本次request请求的变量,用来输出打印并进行跟踪时查找,但在并发场景下,这个变量可能会被其他request请求覆盖掉,所以我们要为每个request请求设置变量副本,供当前线程使用。并且一个request 请求即为一个线程。

  • 实现
    当在多线程并发情况下,有一个共享变量,不同线程设置不同值后,各线程只想获取自己设置的值,这个时候需要用到ThreadLocal。

  • 源码

  1. 存储结构
	// 初始容量,必须是 2 的幂
	private static final int INITIAL_CAPACITY = 16;
	// 存储数据的哈希表
	//这个table是一个Entry类型的数组,Entry是ThreadLocalMap的一个内部类。
	//Entry 用于保存一个键值对,其中key以“弱引用”的方式保存。
	private Entry[] table;
	// table 中已存储的条目数
	private int size = 0;
	// 表示一个阈值,当 table 中存储的对象达到该值时就会扩容
	private int threshold;
	// 设置 threshold 的值
	private void setThreshold(int len){
		threshold = len *2/3;
	}
  1. set()方法
   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

首先获取当前线程t,拿到线程的threadLocals属性(ThreadLocalMap类型),如果没有当前线程的map信息就创建createMap,否则进行set操作。

  • 详细流程
   private void set(ThreadLocal key, Object value) {
	Entry[] tab = table;
	int len = tab.length;
	// 计算要存储的索引位置
	int i = key.threadLocalHashCode & (len-1);
	// 循环判断要存放的索引位置是否已经存在 Entry,若存在,进入循环体
	for (Entry e = tab[i];
	e != null;
	e = tab[i = nextIndex(i, len)]) {
		ThreadLocal k = e.get();
		// 若索引位置的 Entry 的 key 和要保存的 key 相等,则更新该 Entry 的值
		if (k == key) {
			e.value = value;
			return;
		}
		// 若索引位置的 Entry 的 key 为 null(key 已经被回收了),表示该位置的 Entry 已经无效,用要保存的键值替换该位置上的 Entry
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}
	// 要存放的索引位置没有 Entry,将当前键值作为一个 Entry 保存在该位置
	tab[i] = new Entry(key, value);
	// 增加 table 存储的条目数
	int sz = ++size;
	// 清除一些无效的条目并判断 table 中的条目数是否已经超出阈值
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash(); // 调整 table 的容量,并重新摆放 table 中的 Entry
}
  1. get()方法
   public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

   private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

   protected T initialValue() {
        return null;
    }

前面流程和set一致,当ThreadLocalMap 为空时调用setInitialValue方法,默认返回null,不为空时获取map的Entry对象返回结果value。

  • 详细流程
  private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //在key计算hash的位置上直接查询,命中返回该entry
            if (e != null && e.get() == key)
                return e;
            else
                // 没有直接命中,调用getEntryAfterMiss
                return getEntryAfterMiss(key, i, e);
        }
 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            // 从i位置开始遍历,寻找key能对应上的entry
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    // 遇到key为null的entry,调用expungeStaleEntry方法
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
  private int expungeStaleEntry(int staleSlot) {
      Entry[] tab = table;
      int len = tab.length;
  
      // expunge entry at staleSlot
      tab[staleSlot].value = null;
      tab[staleSlot] = null;
      size--;     // 以上代码,将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了
  
     // Rehash until we encounter null
     Entry e;
     int i;
     for (i = nextIndex(staleSlot, len); // 从staleSlot后一个index开始向后遍历,直到遇到为null的entry
          (e = tab[i]) != null;
          i = nextIndex(i, len)) {
         ThreadLocal<?> k = e.get();
         if (k == null) {    // 如果entry的key为null,则清除掉该entry
             e.value = null;
             tab[i] = null;
             size--;
         } else {
             int h = k.threadLocalHashCode & (len - 1);
             if (h != i) {   // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的
                tab[i] = null;
 
                 // Unlike Knuth 6.4 Algorithm R, we must scan until
                 // null because multiple entries could have been stale.
                 while (tab[h] != null)      // 对该entry,重新进行hash并解决冲突
                     h = nextIndex(h, len);
                 tab[h] = e;
             }
         }
     }
     return i;   // 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值
 }
 

expungeStaleEntry方法不止清理了staleSlot位置上的entry,还把staleSlot之后的key为null的entry都清理了,并且顺带将一些有哈希冲突的entry给填充回可用的index中。

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

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

从上图中我们可以看到 entry 为弱引用,所以我们在执行完逻辑后一定要调用remove方法

  • 原因
  1. 数据问题:线程是被线程池复用的,如果在线程处理业务结束的时候,不做remove操作,那么下个业务请求复用到这个线程的时候,也会用线程的ThreadLocal里面的变量执行业务逻辑。
  2. 内存泄漏:如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。
  • 如何解决或者防止内存泄漏

      每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

  • 如何获取主线程变量?

      使用 InheritableThreadLocal

  • 扩展之Synchonized的区别
    ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized本质的区别是:Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值