Java多线程学习三十一:ThreadLocal 是用来解决共享资源的多线程访问的问题吗?

ThreadLocal 是不是用来解决共享资源的多线程访问的。

这是一个常见的面试问题,如果被问到了 ThreadLocal,则有可能在你介绍完它的作用、注意点等内容之后,再问你:ThreadLocal 是不是用来解决共享资源的多线程访问的呢?假如遇到了这样的问题,其思路一定要清晰。这里我给出一个参考答案。

ThreadLocal 并不是用来解决共享资源问题的虽然 ThreadLocal 确实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。所以这道题其实是有一定陷阱成分在内的。

ThreadLocal 解决线程安全问题的时候,相比于使用“锁”而言,换了一个思路,把资源变成了各线程独享的资源,非常巧妙地避免了同步操作。具体而言,它可以在 initialValue 中 new 出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。这是 ThreadLocal 解决并发问题的最主要思路。

如果我们把放到 ThreadLocal 中的资源用 static 修饰,让它变成一个共享资源的话,那么即便使用了 ThreadLocal,同样也会有线程安全问题。如果我们在 SimpleDateFormat 之前加上一个 static 关键字来修饰,并且把这个静态对象放到 ThreadLocal 中去存储的话,代码如下所示:

复制代码
public class ThreadLocalStatic {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");


    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalStatic().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return ThreadLocalStatic.dateFormat;
        }
    }
}
那么在多线程中去获取这个资源并且同时使用的话,同样会出现时间重复的问题,运行结果如下。

复制代码
00:15
00:15
00:05
00:16
...
可以看出,00:15 被多次打印了,发生了线程安全问题。也就是说,如果我们需要放到 ThreadLocal 中的这个对象是共享的,是被 static 修饰的,那么此时其实根本就不需要用到 ThreadLocal,即使用了 ThreadLocal 并不能解决线程安全问题。

相反,我们对于这种共享的变量,如果想要保证它的线程安全,应该用其他的方法,比如说可以使用 synchronized 或者是加锁等其他的方法来解决线程安全问题而不是使用 ThreadLocal,因为这不是 ThreadLocal 应该使用的场景。

这个问题回答到这里,可能会引申出下面这个问题。

ThreadLocal 和 synchronized 是什么关系


既然说 ThreadLocal 和 synchronized 它们两个都能解决线程安全问题,那么 ThreadLocal 和 synchronized 是什么关系呢?

当 ThreadLocal 用于解决线程安全问题的时候,也就是把一个对象给每个线程都生成一份独享的副本的,在这种场景下,ThreadLocal 和 synchronized 都可以理解为是用来保证线程安全的手段。例如,在第 44 讲 SimpleDateFormat 的例子中,我们既使用了 synchronized 来达到目的,也使用了 ThreadLocal 作为实现方案。但是效果和实现原理不同:

ThreadLocal 是通过让每个线程独享自己的副本,避免了资源的竞争。
synchronized 主要用于临界资源的分配,在同一时刻限制最多只有一个线程能访问该资源。
相比于 ThreadLocal 而言,synchronized 的效率会更低一些,但是花费的内存也更少。在这种场景下,ThreadLocal 和 synchronized 虽然有不同的效果,不过都可以达到线程安全的目的。

但是对于 ThreadLocal 而言,它还有不同的使用场景。比如当 ThreadLocal 用于让多个类能更方便地拿到我们希望给每个线程独立保存这个信息的场景下时(比如每个线程都会对应一个用户信息,也就是 user 对象),在这种场景下,ThreadLocal 侧重的是避免传参,所以此时 ThreadLocal 和 synchronized 是两个不同维度的工具。

 

 

-----------------多个 ThreadLocal 在 Thread 中的 threadlocals 里是怎么存储的--------

 

Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系


在讲解本课时之前,先要搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系。我们用最直观、最容易理解的图画的方式来看看它们三者的关系:

我们看到最左下角的 Thread 1,这是一个线程,它的箭头指向了  ThreadLocalMap 1,其要表达的意思是,每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,在这里 Thread 1 所拥有的成员变量就是 ThreadLocalMap 1。

而这个 ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对。那么我们就来看一下它的 key 和 value 分别是什么。可以看到这个表格的左侧是 ThreadLocal 1、ThreadLocal 2…… ThreadLocal n,能看出这里的 key 就是 ThreadLocal 的引用。

而在表格的右侧是一个一个的 value,这就是我们希望 ThreadLocal 存储的内容,例如 user 对象等。

这里需要重点看到它们的数量对应关系:一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。

通过这张图片,我们就可以搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者在宏观上的关系了。

源码分析
知道了它们的关系之后,我们再来进行源码分析,来进一步地看到它们内部的实现。

get 方法
首先我们来看一下 get 方法,源码如下所示:

复制代码
public T get() {
    //获取到当前线程
    Thread t = Thread.currentThread();
    //获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //获取 ThreadLocalMap 中的 Entry 对象并拿到 Value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果线程内之前没创建过 ThreadLocalMap,就创建
    return setInitialValue();
}
这是 ThreadLocal 的 get 方法,可以看出它利用了 Thread.currentThread 来获取当前线程的引用,并且把这个引用传入到了 getMap 方法里面,来拿到当前线程的 ThreadLocalMap。

然后就是一个 if ( map != null ) 条件语句,那我们先来看看 if (map == null) 的情况,如果 map == null,则说明之前这个线程中没有创建过 ThreadLocalMap,于是就去调用 setInitialValue 来创建;如果 map != null,我们就应该通过 this 这个引用(也就是当前的 ThreadLocal 对象的引用)来获取它所对应的 Entry,同时再通过这个 Entry 拿到里面的 value,最终作为结果返回。

值得注意的是,这里的 ThreadLocalMap 是保存在线程 Thread 类中的,而不是保存在 ThreadLocal 中的。

getMap 方法
下面我们来看一下 getMap 方法,源码如下所示:

复制代码
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
可以看到,这个方法很清楚地表明了 Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap 是线程的一个成员变量。这个方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 null,代码如下:

复制代码
ThreadLocal.ThreadLocalMap threadLocals = null;
set 方法
下面我们再来看一下 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);
}
set 方法的作用是把我们想要存储的 value 给保存进去。可以看出,首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap ;然后,如果 map == null 则去创建这个 map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。

可以看出,map.set(this, value)  传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了。

ThreadLocalMap 类,也就是 Thread.threadLocals
下面我们来看一下 ThreadLocalMap 这个类,下面这段代码截取自定义在 ThreadLocal 类中的 ThreadLocalMap 类:

复制代码
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;
//...
}
ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量,其中最重要的就是截取出的这段代码中的 Entry 内部类。在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。我们可以把 Entry 理解为一个 map,其键值对为:

键,当前的 ThreadLocal;
值,实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。
ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同。

比如其中一个不同点就是,我们知道 HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链,如下图所示:

但是 ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。这是 ThreadLocalMap 和 HashMap 在处理冲突时不一样的点。

以上就是本节课的内容。

在本节课中,我们主要分析了 Thread、 ThreadLocal 和 ThreadLocalMap 这三个非常重要的类的关系。用图画的方式表明了它们之间的关系:一个 Thread 有一个 ThreadLocalMap,而 ThreadLocalMap 的 key 就是一个个的 ThreadLocal,它们就是用这样的关系来存储并维护内容的。之后我们对于 ThreadLocal 的一些重要方法进行了源码分析。

 

================内存泄漏——为何每次用完 ThreadLocal 都要调用 remove()===============

 

首先,我们要知道这个事情和内存泄漏有关,所以就让我们先来看一下什么是内存泄漏。

什么是内存泄漏
内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。

因为通常情况下,如果一个对象不再有用,那么我们的垃圾回收器 GC,就应该把这部分内存给清理掉。这样的话,就可以让这部分内存后续重新分配到其他的地方去使用;否则,如果对象没有用,但一直不能被回收,这样的垃圾对象如果积累的越来越多,则会导致我们可用的内存越来越少,最后发生内存不够用的 OOM 错误。

下面我们来分析一下,在 ThreadLocal 中这样的内存泄漏是如何发生的。

Key 的泄漏
在上一讲中,我们分析了 ThreadLocal 的内部结构,知道了每一个 Thread 都有一个 ThreadLocal.ThreadLocalMap 这样的类型变量,该变量的名字叫作 threadLocals。线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。

我们可能会在业务代码中执行了 ThreadLocal instance = null 操作,想清理掉这个 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。

GC 在垃圾回收的时候会进行可达性分析,它会发现这个 ThreadLocal 对象依然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。

JDK 开发者考虑到了这一点,所以 ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用,代码如下所示:

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

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
可以看到,这个 Entry 是 extends WeakReference。弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止 GC。因此,这个弱引用的机制就避免了 ThreadLocal 的内存泄露问题。

这就是为什么 Entry 的 key 要使用弱引用的原因。

Value 的泄漏
可是,如果我们继续研究的话会发现,虽然 ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用,还是刚才那段代码:

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


    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
可以看到,value = v 这行代码就代表了强引用的发生。

正常情况下,当线程终止,key 所对应的 value 是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,如果线程迟迟不会终止,那么可能 ThreadLocal 以及它所对应的 value 早就不再有用了。在这种情况下,我们应该保证它们都能够被正常的回收。

为了更好地分析这个问题,我们用下面这张图来看一下具体的引用链路(实线代表强引用,虚线代表弱引用):

可以看到,左侧是引用栈,栈里面有一个 ThreadLocal 的引用和一个线程的引用,右侧是我们的堆,在堆中是对象的实例。

我们重点看一下下面这条链路:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例

这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个 Value 就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个 Value 了,此时也就发生了内存泄漏问题。

JDK 同样也考虑到了这个问题,在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。

但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏。

如何避免内存泄露
分析完这个问题之后,该如何解决呢?解决方法就是我们本课时的标题:调用 ThreadLocal 的 remove 方法。调用这个方法就可以删除对应的 value 对象,可以避免内存泄漏

我们来看一下 remove 方法的源码:

复制代码
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
可以看出,它是先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法。这里的 remove 方法可以把 key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了。

所以,在使用完了 ThreadLocal 之后,我们应该手动去调用它的 remove 方法,目的是防止内存泄漏的发生。

引用:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=285

 

 

 

 

 

 

 

 

 


 
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值