还不理解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对象。




以上,若存在表述不明确或表述错误的地方,请指正,谢谢!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值