还不理解ThreadLocal的看过来

    技术2025-05-22  52

    文章目录

    ThreadLocal是什么?ThreadLocal与Thread、ThreadLocalMap是什么关系?ThreadLocal 有哪些常用方法?ThreadLocal使用须知ThreadLocal使用举例ThreadLocal为什么会内存泄露?ThreadLocal如何避免内存泄露?

    ThreadLocal是什么?

    ThreadLocal是Java类库提供的在多线程环境下保证对共享资源安全访问的类

    ThreadLocal与Thread、ThreadLocalMap是什么关系?

    通过对源码分析发现,ThreadLocalMap是每一个线程Thread类的成员变量,里面有一个键值对数据Entry[] table,可以认为是一个map。 一个Thread对象持有一个ThreadLocalMap成员变量,而ThreadLocalMap依托Entry静态类来存储数据,Entry结构中key表示ThreadLocal,value表示要存储的数据

    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;

    ThreadLocal 有哪些常用方法?

    initialValue 该方法会返回当前线程对应的初始值。

    此方法默认实现返回null。

    protected T initialValue() { return null; }

    可以使用匿名内部类的方式重写initialValue(),以便在后续使用中可以初始化副本对象。

    这是一个延迟加载的方法,只有在调动get方法的时候才会出触发。 当线程第一次使用get访问变量时,将调用此方法。若线程先调用了set方法,则不会再调用initialValue方法。

    请看源代码,便一目了然为什么这么说了

    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(); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }

    当第一次调用get()时,map为null,代码流转到setInitialValue方法,在setInitialValue方法中首先会去读取initialValue()初始化的值。如果有重写initialValue方法,则会走到我们重写的方法里

    每个线程最多调用一次这个方法。但是如果已经调用了remove()后,再调用get()依然可以调用此方法。

    public class ThreadLocalDemo { // private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { // @Override // protected Integer initialValue() { // return 0; // } // }; private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> { System.out.println("我是InitialValue方法"); return 0; }); public static void main(String[] args) { // threadLocal.set(1); System.out.println(threadLocal.get()); System.out.println(threadLocal.get()); System.out.println("即将执行remove方法"); threadLocal.remove(); System.out.println(threadLocal.get()); } }

    输出结果:

    我是InitialValue方法 0 0 即将执行remove方法 我是InitialValue方法 0

    set(T t) 为线程设置一个新值 T get() 得到这个线程对应的value void remove() 删除对应这个线程的值

    ThreadLocal使用须知

    1、在ThreadLocal使用之前,一定要使用initialValue初始化或set(T t)赋初值,否则可能会报空指针异常

    public class ThreadLocalDemo { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); private static int getValue(){ return threadLocal.get(); } public static void main(String[] args) { System.out.println(ThreadLocalDemo.getValue()); } }

    结论:ThreadLocal#initialValue默认实现返回null,而Integer->int需要拆箱,诱发空指针异常。若getValue()返回Integer,在上面的程序不会报错,但在使用这个数据时依然可能报错。

    2、不要重复造轮子,优先使用框架提供出来的工具类。

    ThreadLocal使用举例

    就以SimpleDateFormat为例,看看ThreadLocal是怎么帮助其实现线程安全的?

    演示多线程下使用SimpleDateFormat格式化时间

    public class ThreadNotSafeDemo{ private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); public static void main(String[] args) { BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("wojiushiwo-pool-%d").build(); ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 1; i <= 50; i++) { long num = i; executor.submit(() -> { String format = dateFormat.format(System.currentTimeMillis()+num*1000); System.out.println(Thread.currentThread().getName() + "当前时间:" + format); }); } executor.shutdown(); } }

    上面的代码演示了使用线程池执行50个解析时间的任务。由于每个任务中的时间都是System.currentTimeMillis()+num*1000不会重复,所以解析出来的时间也应该不重复才对。 输出结果:

    wojiushiwo-pool-1当前时间:2020-07-04 02:08:19 wojiushiwo-pool-2当前时间:2020-07-04 02:08:20 wojiushiwo-pool-3当前时间:2020-07-04 02:08:21 wojiushiwo-pool-4当前时间:2020-07-04 02:08:22 wojiushiwo-pool-5当前时间:2020-07-04 02:08:23 wojiushiwo-pool-6当前时间:2020-07-04 02:08:24 wojiushiwo-pool-7当前时间:2020-07-04 02:08:25 wojiushiwo-pool-8当前时间:2020-07-04 02:08:26 wojiushiwo-pool-9当前时间:2020-07-04 02:08:27 wojiushiwo-pool-10当前时间:2020-07-04 02:08:28 wojiushiwo-pool-1当前时间:2020-07-04 02:08:29 wojiushiwo-pool-1当前时间:2020-07-04 02:08:30 wojiushiwo-pool-3当前时间:2020-07-04 02:08:31 wojiushiwo-pool-3当前时间:2020-07-04 02:08:32 wojiushiwo-pool-5当前时间:2020-07-04 02:08:33 wojiushiwo-pool-6当前时间:2020-07-04 02:08:35 wojiushiwo-pool-7当前时间:2020-07-04 02:08:35 wojiushiwo-pool-8当前时间:2020-07-04 02:08:36 wojiushiwo-pool-9当前时间:2020-07-04 02:08:37 wojiushiwo-pool-10当前时间:2020-07-04 02:08:39 wojiushiwo-pool-1当前时间:2020-07-04 02:08:39 wojiushiwo-pool-2当前时间:2020-07-04 02:08:40 wojiushiwo-pool-4当前时间:2020-07-04 02:08:41 wojiushiwo-pool-4当前时间:2020-07-04 02:08:42 wojiushiwo-pool-5当前时间:2020-07-04 02:08:43 wojiushiwo-pool-6当前时间:2020-07-04 02:08:44 wojiushiwo-pool-7当前时间:2020-07-04 02:08:45 wojiushiwo-pool-8当前时间:2020-07-04 02:08:46 wojiushiwo-pool-9当前时间:2020-07-04 02:08:47 wojiushiwo-pool-9当前时间:2020-07-04 02:08:48 wojiushiwo-pool-1当前时间:2020-07-04 02:08:50 wojiushiwo-pool-2当前时间:2020-07-04 02:08:50 wojiushiwo-pool-1当前时间:2020-07-04 02:08:52 wojiushiwo-pool-3当前时间:2020-07-04 02:08:52 wojiushiwo-pool-1当前时间:2020-07-04 02:08:53 wojiushiwo-pool-6当前时间:2020-07-04 02:08:55 wojiushiwo-pool-1当前时间:2020-07-04 02:08:55 wojiushiwo-pool-8当前时间:2020-07-04 02:08:56 wojiushiwo-pool-1当前时间:2020-07-04 02:08:57 wojiushiwo-pool-8当前时间:2020-07-04 02:08:58 wojiushiwo-pool-2当前时间:2020-07-04 02:08:59 wojiushiwo-pool-4当前时间:2020-07-04 02:09:00 wojiushiwo-pool-2当前时间:2020-07-04 02:09:01 wojiushiwo-pool-3当前时间:2020-07-04 02:09:02 wojiushiwo-pool-7当前时间:2020-07-04 02:09:03 wojiushiwo-pool-6当前时间:2020-07-04 02:09:04 wojiushiwo-pool-10当前时间:2020-07-04 02:09:05 wojiushiwo-pool-9当前时间:2020-07-04 02:09:07 wojiushiwo-pool-1当前时间:2020-07-04 02:09:07 wojiushiwo-pool-8当前时间:2020-07-04 02:09:08

    发现结果中时间2020-07-04 02:08:50有重复现象,说明SimpleDateFormat在多线程环境下不是线程安全的。

    令SimpleDateFormat线程安全的方式有多种,这里主要讨论ThreadLocal

    下面以ThreadLocal来演示实现SimpleDateFormat线程安全的输出时间

    public class ThreadSafeDemo { private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); public static void main(String[] args) { BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("wojiushiwo-pool-%d").build(); ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 1; i <= 50; i++) { long num = i; executor.submit(() -> { String format = threadLocal.get().format(System.currentTimeMillis()+num*1000); System.out.println(Thread.currentThread().getName() + "当前时间:" + format); }); } executor.shutdown(); } }

    输出结果:

    wojiushiwo-pool-15当前时间:2020-07-04 02:20:23 wojiushiwo-pool-15当前时间:2020-07-04 02:19:59 wojiushiwo-pool-15当前时间:2020-07-04 02:20:00 wojiushiwo-pool-15当前时间:2020-07-04 02:20:01 wojiushiwo-pool-15当前时间:2020-07-04 02:20:02 wojiushiwo-pool-15当前时间:2020-07-04 02:20:03 wojiushiwo-pool-15当前时间:2020-07-04 02:20:04 wojiushiwo-pool-15当前时间:2020-07-04 02:20:05 wojiushiwo-pool-15当前时间:2020-07-04 02:20:06 wojiushiwo-pool-15当前时间:2020-07-04 02:20:07 wojiushiwo-pool-15当前时间:2020-07-04 02:20:08 wojiushiwo-pool-15当前时间:2020-07-04 02:20:09 wojiushiwo-pool-15当前时间:2020-07-04 02:20:10 wojiushiwo-pool-14当前时间:2020-07-04 02:20:22 wojiushiwo-pool-1当前时间:2020-07-04 02:19:49 wojiushiwo-pool-15当前时间:2020-07-04 02:20:11 wojiushiwo-pool-14当前时间:2020-07-04 02:20:12 wojiushiwo-pool-1当前时间:2020-07-04 02:20:13 wojiushiwo-pool-15当前时间:2020-07-04 02:20:14 wojiushiwo-pool-14当前时间:2020-07-04 02:20:15 wojiushiwo-pool-1当前时间:2020-07-04 02:20:16 wojiushiwo-pool-15当前时间:2020-07-04 02:20:17 wojiushiwo-pool-14当前时间:2020-07-04 02:20:18 main当前时间:2020-07-04 02:20:24 wojiushiwo-pool-5当前时间:2020-07-04 02:19:53 wojiushiwo-pool-5当前时间:2020-07-04 02:20:25 wojiushiwo-pool-4当前时间:2020-07-04 02:19:52 wojiushiwo-pool-15当前时间:2020-07-04 02:20:26 wojiushiwo-pool-14当前时间:2020-07-04 02:20:27 wojiushiwo-pool-5当前时间:2020-07-04 02:20:28 wojiushiwo-pool-4当前时间:2020-07-04 02:20:29 wojiushiwo-pool-1当前时间:2020-07-04 02:20:30 wojiushiwo-pool-15当前时间:2020-07-04 02:20:31 wojiushiwo-pool-14当前时间:2020-07-04 02:20:32 wojiushiwo-pool-5当前时间:2020-07-04 02:20:33 wojiushiwo-pool-4当前时间:2020-07-04 02:20:34 wojiushiwo-pool-1当前时间:2020-07-04 02:20:35 wojiushiwo-pool-7当前时间:2020-07-04 02:19:55 wojiushiwo-pool-7当前时间:2020-07-04 02:20:36 wojiushiwo-pool-14当前时间:2020-07-04 02:20:37 wojiushiwo-pool-5当前时间:2020-07-04 02:20:38 wojiushiwo-pool-10当前时间:2020-07-04 02:19:58 wojiushiwo-pool-11当前时间:2020-07-04 02:20:19 wojiushiwo-pool-6当前时间:2020-07-04 02:19:54 wojiushiwo-pool-3当前时间:2020-07-04 02:19:51 wojiushiwo-pool-12当前时间:2020-07-04 02:20:20 wojiushiwo-pool-8当前时间:2020-07-04 02:19:56 wojiushiwo-pool-13当前时间:2020-07-04 02:20:21 wojiushiwo-pool-2当前时间:2020-07-04 02:19:50 wojiushiwo-pool-9当前时间:2020-07-04 02:19:57

    ThreadLocal为什么会内存泄露?

    前面有讨论过ThreadLocalMap中静态Entry类,这里详细说下

    static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

    首先Entry继承自弱引用,并且其构造函数中k是使用WeakReference赋值的。所以可以断定Entry中key是弱引用。而value毫无疑问是强引用。

    由JVM知识可以得知,弱引用在垃圾回收时会被主动回收,而强引用只有当GC触发时才会被回收。

    正常情况下,当线程终止时,保存在ThreadLocal里的value会被垃圾回收。但是,如果线程不终止(如线程需要保持很久),那么key对应的value就不会被回收,就会导致内存无法被回收,最终可能出现OOM。 幸好,ThreadLocal中set、remove、rehash方法中会扫描key为null的Entry,并将对应的value也设置为null,这样value就可以被回收了。

    若像上面说的,ThreadLocal不再使用,但线程未终止而且没有显式调用set、remove、rehash等方法,那么内存中的调用链就一直存在,极易引起内存泄露。

    ThreadLocal如何避免内存泄露?

    在使用完ThreadLocal之后手动调用remove方法,删除对应的Entry对象。


    以上,若存在表述不明确或表述错误的地方,请指正,谢谢!
    Processed: 0.014, SQL: 9