ThreadLocal源码解析

前言

ThreadLocal提供了线程的本地实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。主要的作用是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的。

总的来说,ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

正文

1.ThreadLocal底层原理?

首先我先说下它的使用:

ThreadLocal<String> localName  = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();

其实使用很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程要在remove之前去get,都能拿到之前set的值,注意是remove之前进行get。

他是能做到线程间数据隔离的,所以别的线程使用get()方法是没办法拿到其他线程的值,但是有办法是可以拿到的,我后面会说。

至于为什么要remove?这里先简单说下,后面进行详细解释。因为ThreadLocal存在内存泄漏问题,所以在最后最好都进行一次remove操作。

2.ThreadLocal get源码实现过程?

我们先来看看get源码:

public T get() {
    //获得当前线程
    Thread t = Thread.currentThread();
    //每个线程 都有一个自己的ThreadLocalMap,
    //ThreadLocalMap里就保存着所有的ThreadLocal变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //ThreadLocalMap的key就是当前ThreadLocal对象实例,
        //多个ThreadLocal变量都是放在这个map中的
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //从map里取出来的值就是我们需要的这个ThreadLocal变量
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map没有初始化,那么在这里初始化一下
    return setInitialValue();
}
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,所谓的ThreadLocal变量就是保存在每个线程的map中。这个map就是Thread对象中的threadLocals字段

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
        return getEntryAfterMiss(key, i, e);
}

这里解释下ThreadLocal的get方法是如何拿到当前线程所对应的ThreadLocal中的值?

  1. 获取当前线程Thread,也就是调用Thread.currentThread()方法。
  2. 通过Thread获取ThreadLocalMap(每个线程都有一个自己的ThreadLocalMap,ThreadLocalMap里就保存着所有的ThreadLocal变量,ThreadLocalMap的key就是当前ThreadLocal对象实例,多个ThreadLocal变量都是放在这个map中的),也就是调用getMap(…)方法。
  3. 通过当前ThreadLocal对象实例获取ThreadLocalMap中的Entry,也就是调用getEntry(…)方法。Entry的key就是当前线程的ThreadLocal,value是当前线程存储的值。

ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entry的key都是一个弱引用:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //key就是一个弱引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

这样设计的好处是,如果这个变量不再被其它对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露(注意,Entry中的value,依然是强引用,如何回收,见下文分解)

3.ThreadLocal的内存泄露问题?

虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。这个value的引用链条如下:

在这里插入图片描述

可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄露的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理。

以getEntry()为例:

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                // 如果找到kye,直接返回
                return e;
            else
                // 如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
                return getEntryAfterMiss(key, i, e);
        }

下面是getEntryAfterMiss()的实现:

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                // 整个e是一个Entry,也就是一个弱引用
                ThreadLocal<?> k = e.get();
                // 如果找到了,就返回
                if (k == key)
                    return e;
                if (k == null)
                    // 如果key为null,说明弱引用已经被回收了
                    // 那么就要在这里回收里面的value了
                    expungeStaleEntry(i);
                else
                    // 如果key不是要找到的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用这个方法进行value的清理。

从这里可以看到,ThreadLocal为了避免内存泄漏,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检测key是否被回收,进而再回收value。

先介绍下弱引用:

只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收线程扫描它所管辖的内存区域的过程中,一旦发现了只有弱引用对象,不管当前内存空间足够与否,都会回收它的内存。

不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现哪些只具有弱引用的对象。这就导致了一个问题,ThreadLocal在没有外部强引用的时候,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄漏。

就比如线程池里面的线程,线程是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄漏。

按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了,解决办法就是在最后使用remove就好,我们只要记得在使用的最后用remove把值清空就好。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("张三");
    ……
} finally {
    localName.remove();
}

remove的源码很简单,找到对应的值全部清空,这样在垃圾回收器回收的时候,会自动把它们回收掉。

4.ThreadLocalMap中的Hash冲突如何处理?

ThreadLocalMap作为一个HashMap和java.util.HashMap的实现是不同的。对于java.util.HashMap使用的是链表法来处理冲突:

在这里插入图片描述

但是,对于ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放:

在这里插入图片描述

具体来说,整个set()的过程如下:

        private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            // 根据hash,找到数组中的一个位置
            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;
                }
                // 如果key为null,说明原来的key被回收了,那么就要自动清理
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            // 一旦找到了合适的位置,就把这个Entry放进去
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

5.可以被继承的ThreadLocal—InheritableThreadLocal

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

public static void main(String[] args) {    
final ThreadLocal threadLocal = new InheritableThreadLocal();       
threadLocal.set("帅得一匹");    
Thread t = new Thread() {        
    @Override        
    public void run() {            
      super.run(); 
        System.out.pritln( "张三帅么 =" + threadLocal.get());      
    }    
  };          
  t.start(); 
} 

在子线程中我们能够正常输出那一行日志。那具体是如何传递的呢?

传递逻辑很简单,我们开头Thread代码提到ThreadLocals的时候,其中里面还包含一个变量,就是下图箭头指向的:

在这里插入图片描述

在Thread源码中,我们看看Thread.init初始化创建的时候做了什么:

public class Thread implements Runnable {
  ……
   if (parent.inheritableThreadLocals != null)
      this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  ……
}

我截取部分源码,如果父线程的inheritableThreadLocals不为空,那么我们就把父线程的inheritableThreadLocals给当前线程的inheritableThreadLocals。

6.ThreadLocalMap扩容机制?

通过set方法里的源码,我们知道ThreadLocalMap扩容有两个前提:

  • !cleanSomeSlots(i,sz)
  • size >= threshold

在这里插入图片描述

元素个数大于阈值进行扩容,这个很好理解,那么还有一个前提是什么意思呢?我们来看cleanSomeSlots()做了什么?

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

从当前插入元素位置,往后扫描数组中的元素,判断是否是”stale entry“。”stale entry“表示的是那些key为null的entry,cleanSomeSlots方法就是找到它们,调用expungeStaleEntry方法进行清理,如果找到,则返回true,否则返回false。

:为什么扩容需要看返回值?

因为一旦找到,就调用expungeStaleEntry方法进行清理。

private int expungeStaleEntry(int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;

	// expunge entry at staleSlot
	tab[staleSlot].value = null;
	tab[staleSlot] = null;
	size--;
    ...

从上面源码可以看出,size会减一,那么添加元素元素会导致size加一,cleanSomeSlots一旦找到,则会清理一个或这多个元素,size的值最少一,所以返回true,自然没必要在判断size是否大于等于阈值了。

前提条件一旦满足,则调用rehash方法,此时还未扩容:

        private void rehash() {
            // 清理stale entry,会导致size变化
            expungeStaleEntries();

            // 如果size大于等于3/4阈值,则扩容
            if (size >= threshold - threshold / 4)
                resize();
        }

在上面条件都满足时,resize()是真正的扩容,我们来看下resize()都做了什么操作:

        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();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        // key不为null,重新计算索引位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        // 插入新的数组索引位置
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            // 阈值长度的2/3
            setThreshold(newLen);
            size = count;
            table = newTab;
        }
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

resize()的重要过程已经标注在代码中了,我这里说下最后一个注释:setThreshold()这个方法是在扩容后重新设置阈值,即新长度的2/3。

最后总结下扩容的步骤:首先:判断!cleanSomeSlots(i,sz)和size >= threshold是否满足条件。其次:清理stale entry,会导致size变化,判断size是否大于3/4阈值,若size大于3/4阈值,则进行扩容操作。最后:两倍长度扩容,重新计算索引,扩容的同时也顺便清理掉key为null的元素,即stale entry,不再存入扩容后的数组,然后通过setThreshold()这个方法在扩容后重新设置阈值,即新长度的2/3。

7.ThreadLocal实际应用场景?

场景一:全局存储用户信息

在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。

在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)

对于笔者而言,这个场景使用的比较多,当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类(AuthNHolder)存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空, 中间的过程无需在关注如何获取用户信息,只需要使用工具类的get方法即可。

public class AuthNHolder {
	private static final ThreadLocal<Map<String,String>> loginThreadLocal = new ThreadLocal<Map<String,String>>();

	public static void map(Map<String,String> map){
		loginThreadLocal.set(map);
	}
	public static String userId(){
    		return get("userId");
	}
	public static String get(String key){
    		Map<String,String> map = getMap();
    		return map.get(key);
    }
	public static void clear(){
       loginThreadLocal.remove();
	}
	
}

场景二:解决线程安全问题

在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?

在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题

ThreadLocal在设计之初就是为解决并发问题而提供一种方案,每个线程维护一份自己的数据,达到线程隔离的效果。

总结

ThreadLocal在Java多线程开发中有着十分重要的作用。

在这篇文章中,我们主要介绍了ThreadLocal底层结构、源码get()、set()的实现原理,ThreadLocal存在内存泄漏,以及出现hash冲突是如何解决和用于父子线程间传递数据的特殊ThreadLocal的实现,还介绍了ThreadLocalMap扩容是如何操作的。

最后介绍了ThreadLocal在实际场景的应用。

最后引用我很佩服的一个人经常说的话:你知道的越多,你不知道的越多!

文章参考:

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

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

https://juejin.cn/post/6844903760053927949#heading-20

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值