时间相关操作并发

Java ThreadLocal

2019-01-24  本文已影响77人  都是浮云啊

开篇

ThreadLocal 是 JDK底层提供的一个解决多线程并发问题提供的工具类,它为每个线程提供了一个本地的副本变量机制,实现了和其它线程隔离,并且这种变量只在本线程的生命周期内起作用,可以减少同一个线程内多个方法之间的公共变量传递的复杂度。
举一个比较形象的例子(自己想的,说的不好请多多指正):中学时期,我们经常会有这样一个场景:老师把布置的作业写到黑板上,没有 ThreadLocal 这种机制的话是这样的,有一个很大的作业本,每个学生把自己的作业都写到这一个笔记本上面属于自己的那一块区域上。而我们也知道,实际的场景是每个同学都有一个自己的作业本,把各自的作业抄写到自己的作业本上面,就实现了和其它同学的作业隔离,感觉这个比较像 ThreadLocal 的工作原理。

[TOC]

1. ThreadLocal 应用

1.1 ThreadLocal 使用场景

开篇的时候我们大致知道了 ThreadLocal 是个啥,然后这部分了解下 ThreadLocal 的使用场景:
它并不是为了解决多线程共享变量的问题,比如商品的库存数量这种场景下是不能使用 ThreadLocal 的。ThreadLocal 是多线程都需要使用一个变量,但是这个变量的值不需要各个线程间共享,每个线程都有自己的这个变量的值。ThreadLocal 还有一种场景是 在 API 层,我们经常需要 request 这个参数,我们可能就需要在很多场景下使用这个参数,但是每个方法都把它作为参数的话会让方法的参数过多不好维护,所以我们可以把这些 request 都对应到一个线程上面,一个线程内如果想使用这个参数,直接去取就行了。
简而言之就是每个线程拥有自己的实例,然后实例需要在对应线程的使用的多个方法中共享但是不希望被多线程共享。

ThreadLocal 主要解决2类问题:

  1. 并发问题:使用 ThreadLocal 代替 Synchronized 来保证线程安全,同步机制采用空间换时间 -> 仅仅先提供一份变量,各个线程轮流访问,后者每个线程都持有一份变量,访问时互不影响。
  2. 数据存储问题: ThreadLocal 为变量在每个线程中创建了一个副本,所以每个线程可以访问自己内部的副本变量。
1.2 ThreadLocal 在 Spring 中的使用->解决线程安全问题

一般情况下,只有无状态的 Bean 才会在各个实例中共享,在 Spring 中绝大多数的 Bean 都可以声明为 singleton 单例的,比如一些 request 相关的 非线程安全状态采用了 ThreadLocal 让它们成为线程安全的状态。一般情况下,web 应用划分成 MVC 三层,在不同的层次中编写对应的逻辑,下层通过接口向上层开放功能调用,正常情况下,从接收请求到响应都应该属于同一个线程。而 ThreadLocal 是一个很好的机制,它为每个线程提供了一个独立的变量副本解决了变量并发访问的冲突问题,比 Synchronized 要简单且方便,可以让程序具备更高的并发性.

1.3 以HttpRequest为例说明项目中如何使用ThreadLocal的代码
// 定义一个全局的Filter
public class CommonFilter extends OncePerRequestFilter {

    /**
     * 拦截所有的http请求,需要配置过滤器
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            // 把requesu塞到线程的ThreadLocal中
            RequestManager.setHttpServletRequest(request);
            filterChain.doFilter(request, response);
        } finally {
            // 擦除value为null的防止内存泄露
            RequestManager.removeHttpServletRequest();
        }
    }
}

// http请求管理
public class RequestManager {

    private static ThreadLocal<HttpServletRequest> threadLocal = new ThreadLocal<HttpServletRequest>();
    /**
     * 当前线程加入request
     * @param request
     */
    public static void setHttpServletRequest(HttpServletRequest request){
        if(request != null){
            threadLocal.set(request);
        }
    }

    /**
     * 当前线程获取request,在API接口中可以直接调用这个方法获取当前线程的request对象
     */
    public static HttpServletRequest getHttpServletRequest(){
        return threadLocal.get();
    }

    /**
     * 清理request,释放空间
     */
    public static void removeHttpServletRequest(){
        threadLocal.remove();
    }
}


// Test
@RestController
public class Demo {
    @RequestMapping("/testDemo")
    public void test(String s){
        /**
         * 通过这种方式就可以把请求取出来了,不用每次都在参数上加一个request了
         */
        HttpServletRequest request = RequestManager.getHttpServletRequest();
    }
}

2. ThreadLocal 的实现原理

2.1 ThreadLocal 的结构

ThreadLocal 主要分为2个部分:第一部分是它的一些成员属性,这部分主要和计算哈希值相关的。另一部分是它对外提供的几个API,这些方法可以操作它自己内部非常重要的内部类 ThreadLocalMap 所以说它才是 ThreadLocal 的底层实现。
通过前面基本知道了怎么使用 ThreadLocal 了,并且知道了它可以为每个线程提供一个局部的变量副本,实现了线程之间的数据隔离,提高程序的并发性等,但是我们并不知道它是如何实现这部分的功能的。所以这部分开始读码了解底层原理,在了解原理之前,得先知道 Thread ThreadLocal ThreadLocalMap 之间的关系。概括一下: ThreadLocal 并不是把 Thread 作为 key 副本值作为 value的一种类似 HashMap 的结构。而是每个 Thread 里都有一个 ThreadLocalMap,ThreadLocal 只是操作每个线程的 ThreadLocalMap 而已。

不同的版本下的 ThreeadLocal
早期的 ThreadLocal 是每个 ThreadLocal 类都会去创建一个 Map,然后以线程 id作为 key,要存储的局部变量作为 value,这样就可以达到线程隔离的效果。但是这样的话,这个存储数量是 Thread 的数量决定,当线程销毁之后还要去维护 Map中的那份 k-v 让它也随之销毁。后来的版本是这么设计的:每个线程都维护一个 ThreadlocalMap 哈希表(类似HashMap),这个哈希表的 keyThreadLocal 对象本身,value 是要存储的局部副本值,这样的话存储数量是 ThreadLocal 的数量决定的。当 Thread 销毁之后,ThreadLocalMap 也会被随之销毁,减少内存占用。
ThreadLocalMap的实现原理跟HashMap 差不多,内部有一个 Entry 数组,一个 Entry通常至少包括key,value , 特殊的是这个Entry继承了 WeakReference 也就是说它是弱引用的所以可能会有 内存泄露 的情况。这个后面再说。ThreadLocal 负责管理 ThreadLocalMap ,包括插入,删除 等等.另一方面来说 ThreadLocal 基本上就相当于 门面设计模式中的一个Facade类。key就是 ThreadLocal 对象自己,同时,很重要的一点:就ThreadLocalMap 存储在当前线程对象里面。

2.2 ThreadLocal 的成员属性
public class ThreadLocal<T> {
    /**
     * 自定义哈希码(ThreadLocalMaps的),降低哈希冲突
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 生成下一个哈希码的hashCode,操作是原子的,从0开始
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * 连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * 返回下一个哈希码的hashCode,此方法是一个原子类不停地去加上斐波那契散列数,使得哈希值分布均匀
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

}
2.3 ThreadLocal 的方法

ThreadLocal定义了四个主要的方法,set get remove initalValue
对于ThreadLocal有2点需要注意:

  1. ThreadLocal 实例本身是不存储值,它只是提供了一个在当前线程中找到副本值的key(它自己就是ThreadLocalMap的key)
    2.是ThreadLocal包含在Thread中,而不是Thread包含在 ThreadLocal 中,有些小伙伴会弄错他们的关系。
    get()操作获取ThreadLocal中对应的当前线程存储的值的时候,先得到当前线程的 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap ,然后判断ThreadLocalMap是否存在,如果存在,以当前的 ThreadLocalkey ,调用 ThreadLocalMapgetEntry() 方法获取对应的存储实体 e ,找到对应的value值,也就是我们想要的此线程对应的ThreadLocal值,然后返回。
    /**
     * 此方法第一次调用发生在当线程通过 {@link #get} 方法访问此线程的ThreadLocal值时
     * 除非线程先调用了 {@link #set},在这种情况下,{@code initialValue} 方法才不会被这个线程调用
     * 通常情况下,每个线程最多调用1次这个方法,但是也可能再次调用,比如 {@link #remove} 被调用后,调用get
     * 这个方法仅仅简单返回null{@code null}; 如果程序想要它返回除了null之外的初始值,必须继承重写此方法,
     * 通常使用匿名内部类的方式实现
     * @return 返回当前ThreadLocal的初始值 null
     */
    protected T initialValue() {
        return null;
    }

    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal则通过 {@link #initialValue}方法进行初始化值
     * @return 
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的`ThreadLocalMap`对象
        ThreadLocalMap map = getMap(t);
        // 如果Map存在
        if (map != null) {
            // 以当前ThreadLocal实例对象为key获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 找到对应的存储实体e
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取e对应的value值,也就是我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map不存在,证明此线程没有维护此ThreadlocalMap对象,进行一波初始化操作
        return setInitialValue();
    }

    /**
     * set的变样实现,用于初始化值initialValue
     * 用来代替防止用户重写set而无法初始化
     * @return 返回初始化后的值
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 如果此map村咋洗,调用map,set设置此实体entry
        if (map != null)
            map.set(this, value);
        else
         // map不存在时,调用此方法进行ThreadLocalMap对象初始化并将此entry作为第一个值放进去
            createMap(t, value);
         // 返回设置的value值s
        return value;
    }

    /**
     * 设置此线程对应的ThreadLocal的值,大多数子类不需要重写此方法,
     * 只需要依赖{@link #initialValue} 方法代替设置当前线程对应的ThreadLocal值
     * @param 将要保存在当前线程对应的ThreadLocal值
     * 1. 获取当前线程`Thread`对象,进而获取此线程对象中维护的`ThreadLocalMap`对象
     *2. 判断当前的`ThreadLocalMap`是否存在,如果存在就直接调用map.set设置entry,如果不存在就调用`createMap`进行`ThreadLocalMap`对象的初始化,并将此实体`entry`作为第一个值存放到`ThreadLocalMap`中。
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap实例
        ThreadLocalMap map = getMap(t);
        // 存在就设置此entry
        if (map != null)
            map.set(this, value);
        else
        // 不存在就进行对象初始化并设置entry作为第一个值存入ThreadLocalMap中
            createMap(t, value);
    }

    /**
     * 删除当前线程中保存ThreadLocal对应的实体entry
     * 如果此ThreadLocal变量在当前线程中调用{@linkplain #get read} 方法
     * 则会通过调用{@link #initialValue} 方法进行初始化
     * 除非此值value是通过当前线程内置调用set方法设置
     * 这可能导致在当前线程中多次调用initialValue方法初始化
     * 1. 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
     * 2.  判断`ThreadLocalMap`是否存在,如果存在,调用map.remove,以当前的`ThreadLocal`为key删除对应的`entry`
     */
     public void remove() {
         // 获取当前线程Thread对象,进而获取此线程对象中维护的`ThreadLocalMap`对象
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
         // 如果`ThreadLocalMap`存在调用remove方法删除之,当前ThreadLocal对象为key
             m.remove(this);
     }

    /**
     * 获取当前对象Thread对应维护的ThreadLocalMap
     * @param  当前线程
     * @return 对应的ThreadLocalMap
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
2.4 ThreadLocal的内部类ThreadLocalMap

ThreadLocalMap 其内部利用 Entry 来实现 key-value 的存储,类似 HashMap 的结构 如下代码,从上面代码中可以看出 Entrykey 就是 ThreadLocal ,而value 就是值。同时,``Entry 也继承WeakReference` ,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用,弱引用相关的在总结里有。

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

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

ThreadLocalMap 的源码稍微多了点,我们就看两个最核心的方法

这个 set() 操作和我们在集合了解的 put() 方式有点不一样,虽然他们都是 key-value 结构,不同在于他们解决散列冲突的方式不同。集合Map的put()采用的是拉链法,而ThreadLocalMap的set()则是采用开放定址法,开放地址法就是不会有链式的结构,如果冲突了,以当前位置为基准再找一个判断,直到找到一个空的地址。set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。
get()方法有一个重要的地方当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。

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

    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;

    // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
    int i = key.threadLocalHashCode & (len-1);

    // 采用“线性探测法”,寻找合适位置
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {

        ThreadLocal<?> k = e.get();

        // key 存在,直接覆盖
        if (k == key) {
            e.value = value;
            return;
        }

        // key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
        if (k == null) {
            // 用新元素替换陈旧的元素
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

    int sz = ++size;

    // cleanSomeSlots 清楚陈旧的Entry(key == null)
    // 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

/**用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

8. 总结

先说下什么是强引用,什么是弱引用:

a. 在正常的情况下,使用的普遍是强引用,比如A a = new A();这样创建一个a这个对象,当我们执行a = null;的时候,过一段时间GC会把分配给a的空间回收掉。
b. 基于上面已经有了 A a = new A();,又出现了一个B b = new B(a);这样一个创建对象b这个操作。此时我们执行a = null;这个操作,GC并不会回收分配给a的空间,因为即使a被设置为null,但是b仍然持有对象a的引用,所以GC不会回收a,这样一来就尴尬了 既不能回收,又不能使用 这种情况就有一个专业的名词叫内存泄露
那么如何处理呢?可以通过b = null;,也可以使用弱引用WeakReference w = new WeakReference(a);。因为使用了弱引用WeakReference,GC是可以回收 a 原先所分配的空间的。
再回到 ThreadLocalMap 的层面来看为啥哈希表的节点要实现WeakReference弱引用。也就是ThreadLocalMap中的key使用Threadlocal实例作为弱引用。如果一个ThreadLocal没有外部引用去引用它,那么在系统GC的时候它势必要被回收的。这样一来ThreadLocalMap中就会出现keynullentry就没有办法访问这些keynullEntryvalue。如果线程一直不能结束的话,就会存在一条强引用链:ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value永远无法被回收造成内存泄露。其实在ThreadLocalMap的设计中为了防止这种情况,也有一些防护措施,比如新增、移除、获取的时候都会去擦除key==nullvalue。但是这些措施并不能保证一定不会内存泄露,比如:
a. 使用了static修饰的ThreadLocal,延长了ThreadLocal的生命周期,可能会导致内存泄露。
b. 分配使用了ThreadLocal又不再调用get set remove方法也会导致内存泄露。
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

官方给的说法是: 为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
我们假设我们自己设计的时候key 使用的强引用和弱引用

  1. key使用强引用:如果引用ThreadLocal的对象ThreadLocalRef被回收了,但是ThreadLocalMap还持有ThreadLocal对象的强引用,如果没有手动删除的话ThreadLocal不会被回收,这样会导致Entry内存泄露
  2. key使用弱引用:引用的ThreadLocal的对象ThreadLocalRef被回收了,由于ThreadLocalMap有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用get(),set(),remove()的时候会被清除。
    比较上面的2种情况,我们会发现:ThreadLocalMap的生命周期和Thread一样长,如果都没有手动删除key都会导致内存泄露。但是弱引用多了一层保障,就是value在下一次ThreadLocalMap 调用 get(),set(),remove() 的时候会被清除。
    因此可知,ThreadLocal发生内存泄露的根源是由于ThreadLocal的生命周期和Thread一样长,在没有手动删除对应的key的时候就会导致内存泄露,并不是因为弱引用导致的,弱引用只是优化的方式。
    综上分析:为了避免内存的泄露,每次使用完 ThreadLocal 的时候都需要调用 remove() 方法来擦除数据。并且大规模网站一般都会使用到线程池,如果没有及时清理的话不仅是内存泄露,业务逻辑可能也会被影响。所以养成好习惯,记得擦除数据。
三者关系

ThreadLocalsynchronized都是用来处理多线程环境下并发访问变量的问题,只是二者处理的角度不同、思路不同。
ThreadLocal 是一个类,通过对当前线程中的局部变量操作来解决不同线程的变量访问的冲突问题。所以ThreadLocal提供了线程安全的共享对象机制,每个线程都拥有其副本。
Java中的synchronized是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量时多个线程共享的。
同步机制(synchronized关键字)采用了以“时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。

正是因为这几点,所以能够实现数据隔离,获取当前线程的局部变量值,和其它线程无关。

在实际开发中,我们经常会使用 ThreadLocal 传递日志的 requestId,以此来获取整条的请求链路记录下来方便排查问题。然而当一个线程中开启了其它的线程,此时的 Threadlocal 里面的数据就会无法获取。比如下面的代码最开始获取到的就是Null。因为不是同一个线程,所以理所当然输出的值为Null,如果要实现父子线程通信,这个问题在 Threadlocal 的子类 InheritableThreadLocal 已经有对应的实现了,通过这个实现,可以实现父子线程之间的数据传递,在子线程中能够使用父线程的 ThreadLocal 本地变量。InheritableThreadLocal 继承了 ThreadLocal 并且重写了三个相关的方法,具体处理大致是 之前的 ThreadLocal 获取 ThreadlocalMap 的时候一般都是用 this ,在这里都是Thread先获取父线程,然后将父线程的 ThreadLocalMap 传递给子线程

/**
 * ThreadLocalTest
 *
 * @author yupao
 * @since 2019/1/22 下午10:52
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        //ThreadLocal<String> threadLocal = new ThreadLocal<>();
        ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set("main thread");
        Thread thread = new Thread(()->{
            // 直接使用ThreadLocal输出 null,使用ThreadLocal输出 main thread
            System.out.println(threadLocal.get());
        });
        thread.start();
    }
}

真的这么好的吗,看下阿里巴巴编码规范插件说了啥?不要显示创建线程,请使用线程池!所以下面我们用线程池来试试!


不规范提示

如下代码所示,当线程池的核心线程数设置为1的时候,2次输出的结果都是 ”我是主线程1“。ThreadPoolManage 是我本地写的一个线程池实现,github上有源码。原因相信都能踩到了,线程池会缓存使用过的线程,第一个任务来的时候创建一个线程,此时线程空闲了,第二次来任务还是会使用这个线程,所以就会出现下面的问题了。如何解决?阿里的transmittable-thread-local 提供了解决方案,思路是,InheritableThreadLocal虽然可以完成父子线程的传递,但是对于使用了线程池的情况线程是让线程池去创建好的,然后拿来复用的,这个时候父子线程传递 ThreadLocalMap 的引用没有意义了,应用需要的是吧任务提交给线程池时候把 ThreadLocalMap 传递到任务去执行。感兴趣在阿里的github上有,已经开源的。

/**
 * ThreadLocalTestExecutor
 *
 * @author yupao
 * @since 2019/1/23 下午11:00
 */
public class ThreadLocalExecutorTest {
    private static ThreadPoolManager threadPoolManager = ThreadPoolManager.INSTANCE;
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set("我是主线程1");
        threadPoolManager.addExecuteTask(()->{
            System.out.println(threadLocal.get());
            return null;
        });
        threadLocal.set("我是主线程2");
        threadPoolManager.addExecuteTask(()->{
            System.out.println(threadLocal.get());
            return null;
        });
        
        //当线程池核心线程数为1的时候2次输出都是 我是主线程1
    }

}
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
上一篇下一篇

猜你喜欢

热点阅读