乐趣区

关于java:JAVA并发编程ThreadLocal的介绍使用及源码分析

1.ThreadLocal 是什么

2.ThreadLocal 能干什么

3.ThreadLocal 在生产中的利用

4.ThreadLocal 源码剖析

5.ThreadLocal 内存泄露问题

6. 总结

1.ThreadLocal 是什么
在开始理解 ThreadLocal 是什么之前,咱们能够在 java 里搜一下这个类,看一下源码的正文:

意思大略就是:
ThreadLocal 提供了 线程公有的局部变量 ,这些变量不同于一般变量,因为每一个线程在拜访 ThreadLocal 实例的时候(通过 get 或者 set 办法) 都有本人的,独立初始化的正本
ThreadLocal 实例通常是类中的公有动态字段, 应用它的目标是心愿将状态(用户 id 或者事务 id)与线程关联起来。

2.ThreadLocal 能干什么
可能听了下面的形容,你还是一头雾水。简略地来说,ThreadLocal 能办到这种事件:
它实现了 每一个线程都有本人的专属本地变量正本 (本人用本人的变量不必麻烦他人,不和其他人共享,人各一份)。
次要解决了 让每个线程绑定本人的值 ,通过应用 get() 和 set()办法,获取默认值或者将其值更改为以后线程所存的正本的值从而 防止了线程平安问题

3.ThreadLocal 在生产中的利用
通过下面的解说,咱们应该明确了 ThreadLocal 到底是什么,以及怎么用的一个概念,然而具体还是不会用,以及一时半会儿不会想到适宜的应用场景(有什么场景是须要在多线程操作的时候,人手一份?)

此时咱们举一个例子,便能马上明确它的应用场景:

咱们的切入点便是————SimpleDateFormat

对于 SimpleDateFormat,咱们应该都很相熟,咱们通常拿这个工具类来转换日期格局,看起来没什么问题,然而一旦应用不小心会有大问题!

咱们先来看一下 SimpleDateFormat 的源码正文:

SimpleDateFormat不是线程平安的 ,举荐为每一个线程创立独立的格局实例。如果多个线程同时拜访一个格局,则它 必须放弃内部同步

假如咱们在工作的时候,常常应用 SimpleDateForma,咱们便将它抽出来独立为一个工具类,可能咱们会 new 一个 SimpleDateForma 对象,而后对此对象进行复用,殊不知,这种写法的多线程下的危险性!


public class DateUtils {
    // 先新建一个日期转换类
    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 模仿并发环境下应用 SimpleDateFormat 的 parse 办法将字符串转换成 Date 对象
     *
     * @param stringDate
     * @return
     * @throws Exception
     */
    public static Date parseDate(String stringDate) throws Exception {return sdf.parse(stringDate);
    }
    
    public static void main(String[] args) throws Exception {for (int i = 1; i < 30; i++) {new Thread(() -> {
                try {System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
                } catch (Exception e) {e.printStackTrace();
                }
            }, String.valueOf(i)).start();}
    }
}

就如上述代码,在多线程并发的状况下,应用同一个工具类,会呈现什么问题呢?咱们来运行一下代码看看:

由此看出,在多线程下对一个日期转换工具类进行操作,很容易出 bug 起因是:
SimpleDateFormat 类外部有一个 Calendar 对象援用,它用来存储和以后 SimpleDateFormat 对象相干的日期信息,这就会导致一个问题:
如果多个线程援用这个 SimpleDateFormat,那么多个线程之间就会共享这个类 ,同时也共享这个 Calendar 援用, 那么可能这个线程还没有转化实现日期,这个日期就被替换掉了,呈现各种并发操作的问题。

针对以上这种状况,咱们有以下四个办法能够解决这个问题:
1)将 SimpleDateFormat 定义成局部变量。
毛病:每调用一次 转换方法都会创立一个 SimpleDateFormat 对象,办法完结后又要作为垃圾回收。
2)加锁。
将这个办法加上 synchronized 串行化,然而会重大影响吞吐量

3)应用别的工具类
阿里巴巴开发手册嵩山版就指出:

4)就是咱们明天要介绍的,应用 ThreadLocal.

public class DateUtils {//public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));


    /**
     * 模仿并发环境下应用 SimpleDateFormat 的 parse 办法将字符串转换成 Date 对象
     *
     * @param stringDate
     * @return
     * @throws Exception
     */
    public static Date parseDate(String stringDate) throws Exception {return SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse(stringDate);
    }

    public static void main(String[] args) throws Exception {for (int i = 1; i < 30; i++) {new Thread(() -> {
                try {System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
                } catch (Exception e) {e.printStackTrace();
                }
            }, String.valueOf(i)).start();}
    }
}

这样,调用它的每一个线程就都应用了一个工具类的拷贝正本,每个线程一份,就不会呈现这样的问题了。

4.ThreadLocal 源码剖析

那么 ThreadLocal 到底是如何办到,每个 Thread 人手一份变量拷贝的呢?咱们来进行一下源码剖析:

咱们发现,Thread 对象外面有一个 ThreadLocal 对象中的 static 类,ThreadLocalMap。

而 ThreadLocalMap 又是由 Entry(ThreadLocal<?> k, Object v) 组成的。

由此咱们能够失去上面这幅图:

threadLocalMap 实际上是一个以 ThreadLocal 为 key,Object 为 value 的对象。当咱们为 threadLocalMap 变量赋值,就是以以后的 ThreadLocal 值为 key,传进来的值为 value 的一个 Etry,并在这个 threadLocalMap 中寄存。

近似的了解为:

1)每一个 Thread 都有本人的一个 ThreadLocal。
2)每一个 Thread 都有一个 ThreadLocalMap,外面保留了一个键值对,以 ThreadLocal 为 key,以传进来的值为 value。
3)每个线程要用到 ThreadLocal 的时候,用以后的线程去 Map 外面获取,这样每个线程便领有了本人的独立变量。
4)人手一份,竞争条件被彻底消除,在并发模式下是相对的平安变量。

5.ThreadLocal 内存泄露问题

其实 ThreadLocal 是存在着一些问题的:内存透露问题。

什么是内存透露:不再会被应用的对象或者变量占用的内存不能被回收,就是内存透露。

在解释这个景象之前,咱们先看一下 Entry, 继承了 WeakReference 类,弱援用。
对于强脆弱虚援用,咱们在后面这篇博客介绍过。
深刻了解 JVM(八)——强脆弱虚援用

强援用:当内存不足,JVM 开始垃圾回收,对于强援用的对象,就算呈现了 OOM 也不会对该对象进行回收。
软援用:内存足够的前提下,不回收该对象。内存不够的前提下,回收该对象
弱援用:不论内存是否够用,只有是弱援用,一律回收。
虚援用:** 在这个对象呗收集器回收的时候收到一个零碎告诉或者后序增加进一步的解决,是 jvm 的技术人员用的,
**

为什么 Entry 用了弱援用?

每当一个 function 执行结束之后,栈帧销毁,此时 ThreadLocal 也就没有了,但此时线程的ThreadLocalMap 里的某个 entry 的 key 援用还指向这个对象。

弱这个 key 是 强援用 ,就会导致 key 指向的 ThreadLocal 对象及 v 指向的 对象不能被 gc 回收 ,造成内存透露。
弱这个 key 是 弱援用 ,就能大概率缩小内存透露的问题( 还有一个 key 为 null 的问题 ),应用弱援用, 就能将 key 援用指向为 null

由此看出,通过弱援用,的确可能肯定水平解决内存透露问题。

不过 gc 结束之后,ThreadLocalMap 就会呈现 key 为 null 的 Entry,就没有方法拜访这些 key 为 null 的 entry 的 value,如果以后线程迟迟不完结(比方线程池的常驻外围线程 )这些 key 为 null 的 entry 就会始终存在一条强援用链, 而且 value 可能会超级大,更进一步地拖垮了服务器的性能。

因而弱援用不能 100% 保障内存不泄露。咱们要在不应用某个 ThreadLocal 对象后,手动调用 remoev 办法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的 ThreadLocalMap 对象也是重复使用的,如果咱们不手动调用 remove 办法,那么前面的线程就有可能获取到上个线程遗留下来的 value 值,造成 bug。

上图这个办法就是用来解决 null key 的内存透露问题的。

此外 set()办法:

get()办法:

都做了空 key 的判断

6. 总结
1)ThreadLocal 并不解决线程间共享数据的问题
2)ThreadLocal 实用于变量在线程间隔离且在办法间共享的场景
3)ThreadLocal 通过隐式的在不同县城内创立独立实例正本且防止了实例线程平安的问题
4) 每个线程都持有一个专属于本人的 Map,并保护了 ThreadLocal 对象与具体实例的映射。(该 map 只能被本人拜访,所以没有线程平安问题)
5)ThreadLocalMap 的 Entry 对 ThreadLocal 的援用为弱援用,防止了 ThreadLocal 对象无奈被回收的问题。
6)都会通过 expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个办法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象自身从而避免内存透露,属于平安加固的办法

退出移动版