深入理解Java并发编程之把ThreadLocal扣烂

基本含义

ThreadLocal字面意思是线程局部变量,它为每一个线程提供了独立的互不干扰的局部变量。

  1. ThreadLocal类是一个泛型类,也就是说这个局部变量可以是各种类型,比如:Long,List等等。
  2. ThreadLocal类提供了get和set方法以在线程运行周期内获取和改变这个局部变量的值。
  3. 每一个线程的线程局部变量ThreadLocal是相互独立,互不干扰的
  4. 线程局部变量ThreadLocal可以提供一个初始化方法,对于当前线程没有值的ThreadLocal变量会在第一次get()时,调用初始化方法initialValue进行初始化。该方法是一个延迟调用方法。

下面以一个简单的例子来简单介绍下ThreadLocal的使用:

public class ThreadLocalDemo2 {
    public static class MyRunnable implements Runnable {
        private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
            protected Integer initialValue() {
                return 1;
            }
        };

        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread t1 = new Thread(myRunnable, "A");
        Thread t2 = new Thread(myRunnable, "B");

        t1.start();
        t2.start();
    }
    /**
     B:48
     A:32
     即:线程A与线程B中ThreadLocal保存的整型变量是各自独立的,互不相干,只要在每个线程内部使用set方法赋值,
     然后在线程内部使用get就能取到对应的值。
     */
}
复制代码

实现原理

ThreadLocal的set(T value), get()源码

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    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();
    }
    
    public static native Thread currentThread();
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
复制代码
  1. 不管是set还是get方法,首先通过native currentThread()方法拿到当前运行线程,然后拿到当前线程t对象实例上的类型为ThreadLocalMap的threadLocals字段。
  2. 对于set方法,获取到线程上的ThreadLocalMap后,如果存在直接set值;如果不存在,根据set的值初始化一个Map。
  3. 对于get方法,获取到线程上的ThreadLocalMap后,如果存在直接get获取值并类型转换;如果不存在,调用setInitialValue()方法设置和返回初值。

注意

ThreadLocal所存储的变量的实际值是通过ThreadLocalMap结构存在Thread类的成员变量上的,也就是说每一个Java线程,Thread类的对象实例,都有一个自己的ThreadLocalMap

ThreadLocalMap

ThreadLocalMap是ThreadLocal.java中的一个静态内部类,它是一个为了维护线程局部变量(ThreadLocal)定制化的哈希表。

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

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
		
		...
        private Entry[] table;
		...
		
	}
复制代码
  1. 这个ThreadLocalMap的Key为泛型类ThreadLocal的实例,Value为要存储的ThreadLocal变量T。

  2. 实际ThreadLocal变量T与ThreadLocal的实例一起作为一个Entry,存储在table里面。

  3. 注意到这里的ThreadLocalMap.Entry是继承WeakReference使得作为Key ThreadLocal的实例为一个弱引用。那么,在ThreadLocal的实例仅存在弱引用的且被GC线程扫描到的时候,就会GC回收掉threadLocal实例的内存。这个时候,对应的Key值就为null了。

  4. 这里设计为Map是由于一个线程可能有多个线程局部变量即多个ThreadLocal的对象实例。

 

ThreadLocalMap哈希冲突

上面说到ThreadLocalMap,是一个定制化的哈希表。既然是哈希表就需要解决哈希冲突的问题。对于java.util.HashMap,解决冲突的方式是拉链法

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

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
        
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
复制代码

而ThreadLocalMap解决冲突的方式是开放定址法 。

以set操作为例,简单的说就是通过Key做一次Hash之后,如果发现哈希结果对应位置的key和当前要set的key不一致,就往后面找,直到找到一个空的位置。

那么,可不可能找不到呢?

答案是不可能的。

  1. 上面源码的nextIndex方法保证下标到数组结尾后就又从table数组开头寻找。
  2. 如果找不到,则表示当前table数组已经满了。然而,上面源码保证,每次数组大小达到threshold就会触发扩容resize。所以,扩容一定发生在table数组变满之前。

开放定址法的副作用

不好的副作用 由于使用了开放定址法,导致ThreadLocalMap的set,get,remove操作都不能在一次哈希寻址确定找到正确的位置。需要再花费O(n)的时间进行二次寻址去找到空位置或者是能获取、删除的位置。

好的副作用 JDK源码作者通过另一种方式利用了开放定址法带来的二次寻址的循环。在set和get方法的二次寻址的循环过程中,如果发现了stale entry(即key值为空,但是Entry值非空,这里也可以理解为value值非空)的位置,就会进行清理。

  • ThreadLocalMap -> set -> replaceStaleEntry -> expungeStaleEntry

  • ThreadLocalMap -> get -> getEntry -> getEntryAfterMiss -> 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--;
			...
        }
复制代码

注意:这种方式并不能保证,每次ThreadLocalMap.set 或者 get操作都能清除掉所有的key被回收的entry节点。举一个极端的反例,ThreadLocalMap有key为null的entry,但是get操作的第一次hash就直接找到了正确的位置,并没有进行二次寻找。那么,此时就无法进行清除。

为什么ThreadLocal要用WeakReference

上面提到,ThreadLocalMap::Entry::ThreadLocal是一个弱引用。那么,为什么要用WeakReference呢?

 

这里,我们反向思考下,如果不使用弱引用,而使用强引用。那么,在线程的整个生命周期内,所有定义的ThreadLocal变量都一直存在,即使是用户已经不再使用ThreadLocal变量了,这是因为下面2条的引用关系链一直存在:

  • ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->key

  • ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value

那么,如果用户不进行手动的ThreadLocalMap::remove,所占用的空间就一直释放不掉。

综上,我理解的使用ThreadLocalMap->Entry->key(即ThreadLocal)使用弱引用的原因是为了在用户没有进行手动的ThreadLocalMap::remove情况下,也能让系统有方法在set,get的时候进行部分的资源清理。虽然,JVM只清理了key,但是后续JDK源码设计提供了清理value以及整个entry的机制(将value和entry在ThreadLocalMap中的强引用给消除掉)。但是,这机制不一定能用上

So,每次确定ThreadLocal不再使用后,都要手动调用它的remove()方法进行数据清除。

不然,就可能会出现内存泄露。

内存泄露

在ThreadLocal变量仅持有弱引用的时候,如果经历了GC就会被清除掉内存。然后,ThreadLocalMap的ThreadLocal key就变成null了。但是,对应的value由于上面所写的强引用关系链还一直存在,就没发被回收。于是,就发生了value值没发被获取和使用,但是又无法被回收的情况,即内存泄漏。

应用场景

举几个例子说明一下:

  1. 比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递
  2. 比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。我们先笼统但不正确的分析一次 web 请求的过程:
  • 用户在浏览器中访问 web 页面;
  • 浏览器向服务器发起请求;
  • 服务器上的服务处理程序(例如tomcat)接收请求,并开启一个线程处理请求,期间会使用到 Session ;
  • 最后服务器将请求结果返回给客户端浏览器。

从这个简单的访问过程我们看到正好这个 Session 是在处理一个用户会话过程中产生并使用的,如果单纯的理解一个用户的一次会话对应服务端一个独立的处理线程,那用 ThreadLocal 在存储 Session ,简直是再合适不过了。但是例如 tomcat 这类的服务器软件都是采用了线程池技术的,并不是严格意义上的一个会话对应一个线程。并不是说这种情况就不适合 ThreadLocal 了,而是要在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现。

最后,觉得写的不错的同学麻烦点个赞,支持一下呗^_^~

猜你喜欢

转载自blog.csdn.net/m0_46757769/article/details/106503316