ThreadLocal介绍

注意:文章案例使用了lambda表达式和线程池,阅读前请先对这两种技术做简单了解。

1、介绍

ThreadLocal是维持线程封闭性的一种规范的方法,这个类能使线程中的某个值与保存值得对象关联起来。每个Thread中都有一个ThreadLocalMap,使用ThreadLocal.set方法可以将该值的副本存在当前线程中,ThreadLocal.get方法将保存的值的副本取出。

每个Thread中都有一个ThreadLocalMap,当ThreadLocal.set方法被调用时,值其实是保存在了当前线程的ThreadLocalMap中,key值为ThreadLocal对象,value为当前值。

2、两种用途:

  1. 防止对可变的单实例对象或存在线程安全的全局变量进行共享;
  2. 将上下文保存在当前线程中,避免上下文信息层层传递。

3、案例分析

用途一:对于可变的单实例对象很容易理解,下面只演示对全局变量共享的用途。

我们先看一下错误案例。

代码1:
static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 1000; i++) {
        int finalI = i;
        executorService.execute(() -> {
            Date date = new Date(finalI * 1000);
            System.out.println(simpleDateFormat.format(date));
        });
    }
    executorService.shutdown();
}

如代码1所示,我们将SimpleDateFormat作为全局变量,每个线程都调用同一个SimpleDateFormat对象,因SimpleDateFormat并不是线程安全的,所以最终结果是错误的。

下面我们再看一下使用ThreadLocal的正确案例。

代码2:
static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 1000; i++) {
        int index= i;
        executorService.execute(() -> {
            Date date = new Date(index* 1000);
            SimpleDateFormat simpleDateFormat = dateFormatThreadLocal.get();
            System.out.println(simpleDateFormat.format(date));
        });
    }
    executorService.shutdown();
}

如代码2所示,我们在声明ThreadLocal时,给它赋一个初始值,我们可以通过get方法获得这个初始值的副本,不同的子线程将获得不同的副本,线程之间不会影响,就不会出现线程安全问题。

用途二:将上下文保存在当前线程中,避免上下文信息层层传递。

我们演示一下这种场景。

代码3:
static ThreadLocal<Date> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 2; i++) {
            executorService.execute(() -> {
                Date date = new Date();
                System.out.println(Thread.currentThread().getName() + " set的Date的hashcode:" + System.identityHashCode(date));
                threadLocal.set(date);
                test1();
                threadLocal.remove();
            });
        }
        executorService.shutdown();
    }
    static void test1() {
        test2();
    }
    static void test2() {
        test3();
    }
    static void test3() {
        Date date = threadLocal.get();
        System.out.println(Thread.currentThread().getName() + " get的Date的hashcode:"+ System.identityHashCode(date));
    }

如代码3所示,我们使用线程池创建两个线程,并且在子线程的run方法中声明一个Date对象并调用ThreadLocal的set方法,在test3方法中调用get方法获得Date对象。

我们看看运行结果:

结果4:
pool-1-thread-1 set的Date的hashcode:1070855846
pool-1-thread-2 set的Date的hashcode:1683005194
pool-1-thread-1 get的Date的hashcode:1070855846
pool-1-thread-2 get的Date的hashcode:1683005194

我们通过对比set和get的Date对象的hashcode值,确定是否是同一个Date对象。我们看到子线程pool-1-thread-1和pool-1-thread-2两处的Date对象的hashcode值均相同,验证了用途二并且这种方法是线程安全的。

4、源码分析

下面我们对以上演示进行源码分析,我们首先看一下ThreadLocal的set方法的源码。

源码5:
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

从源码5中看到,进入set方法首先获得当前线程对象,调用getMap方法获得当前对象的ThreadLocalMap,如果ThreadLocalMap为null,就会创建该对象。得到ThreadLocalMap后,将set传入的值保存在Map中,key为当前ThreadLocal对象。在代码3中,即使ThreadLocal为全局变量也不会出现线程安全问题,就是因为set的值并不会保存在ThreadLocal中,而是保存在当前线程中的ThreadLocalMap中。

下面我们看一下withInitial方法的源码。

源码6:
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

SuppliedThreadLocal(Supplier<? extends T> supplier) {
    this.supplier = Objects.requireNonNull(supplier);
}

我们从源码6中看到,通过withInitial传入的匿名内部类的对象,保存在ThreadLocal的子类SuppliedThreadLocal中。

下面我们看一下get方法的源码,是怎么从获取值得。

源码7:
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;
}

@Override
protected T initialValue() {
       return supplier.get();
}

从源码7中看到,调用进入get方法后,首先获得map中的值并返回,我们从源码5中了解到,调用set方法,将值保存到map中,这里对应用途2中的代码3。如果map为null或者map中的值为null,代码则调用setInitialValue方法,进入该方法,第一行又要调用initialValue方法,我们进入子类SuppliedThreadLocal的initialValue,发现该方法获取我们通过ThreadLocal.withInitial方法传入的匿名类对象中的返回值。在setInitialValue方法中得到该值并保存到当前线程的ThreadLocalMap中并返回该值。这儿是怎么实现线程安全的呢?其实通过ThreadLocal.withInitial传入的匿名内部类是Supplier实现,在initialValue方法中每次调用一次supplier.get(),就会执行一次withInitial中的方法体,相对于代码2,就会创建一个SimpleDateFormat对象并返回,所以不同的线程获得的是不同的SimpleDateFormat对象,保存在ThreadLocalMap中的值也是不同的,也就是不同的SimpleDateFormat副本。

5、缺点

ThreadLocal有其强大的地方,但并不是完美的,下面我们说说它的缺点。

缺点1:可能引起资源浪费

我们在代码3中可以发现有一行是threadLocal.remove();字面意思就是删除threadLocal.set保存在值。我们知道在线程池中子线程是循环利用的,因为强引用的关系,threadLocal.set的值并不会因为run方法的执行结果而被回收,所以需要调用remove方法,将值从ThreadLocalMap中删掉,这样就会被垃圾回收期回收。

缺点2:降低代码可重用性,同时引入类之间隐含的耦合性

我们看一下代码3,如果我们想只重用test3方法,threadLocal.get的返回值是null,再严重些回到之空指针异常,因此降低了test3方法的重用性。为了避免这种错误,我们不得不获得threadLocal的对象并set一个Date对象,这样就导致与test3所在的类进行了耦合。

6、总结

ThreadLocal有其强大的地方,应当根据实际的业务需求适当使用。因有其缺点,拒绝滥用,否则得不偿失的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值