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

基本含义

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

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

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

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()方法设置和返回初值。

注意web

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。tomcat

  2. 实际ThreadLocal变量T与ThreadLocal的实例一块儿做为一个Entry,存储在table里面。bash

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

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

ThreadLocalMap哈希冲突

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

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 ,通常能够用拦截器、过滤器来实现。

最后,以为写的不错的同窗麻烦点个赞,支持一下呗^_^~

参考与感谢

  1. juejin.im/post/5a64a5…
  2. blog.csdn.net/eson_15/art…
  3. www.imooc.com/article/267…
  4. www.cnblogs.com/fengzheng/p…
  5. www.jqhtml.com/58671.html
相关文章
相关标签/搜索