ThreadLocal 是 JDK 1.2 提供的一个工具,作者其一也是咱们耳熟能详的大佬 Doug Lea

这个工具次要是为了解决多线程下共享资源的问题

接下来咱们从 ThreadLocal 的定义以及实用场景一步步扒开它的外衣

实用场景

  • 场景1,ThreadLocal 用作保留每个线程独享的对象,为每个线程都创立一个正本,这样每个线程都能够批改本人所领有的正本, 而不会影响其余线程的正本,确保了线程平安。
  • 场景2,ThreadLocal 用作每个线程内须要独立保存信息,以便供其余办法更不便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,后面执行的办法保留了信息后,后续办法能够通过 ThreadLocal 间接获取到,防止了传参,相似于全局变量的概念。

场景1

咱们去饭店点了一桌子菜,有面条,有炒菜,有卤味。这个饭店的厨师很激情,每个厨师都想上面给你吃,第一个厨师给这个面放了一把盐巴,第二个厨师不晓得也给了这个面放了盐巴,第三个厨师不晓得也给了这个面放了盐巴,第四个厨师.......

这就好比多线程下,线程不平安的问题了

所以 Doug Lea 说,你们一人负责做一道菜,不要瞎胡闹

接下来咱们高低代码来演示一下这个简略的例子(100个线程都要用到 SimpleDateFormat)

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 < 100; i++) {        int finalI = i;        threadPool.submit(new Runnable() {            @Override            public void run() {                String date = new ThreadLocalDemo01().date(finalI);                System.out.println(date);            }        });    }    threadPool.shutdown();}public String date(int seconds) {    Date date = new Date(1000 * seconds);    return dateFormat.format(date);}输入:00:0500:0700:0500:0500:0600:0500:0500:1100:0500:1200:10  

执行下面的代码就会发现,控制台所打印进去的和咱们所期待的是不统一的

咱们所期待的是打印进去的工夫是不反复的,然而能够看出在这里呈现了反复,比方第一行和第三行都是 05 秒,这就代表它外部曾经出错了。

这时候是不是有机智的同学说,并发问题加锁不就解决了吗,that is good idea

代码改一下变成这样

    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 ThreadLocalDemo05().date(finalI);                    System.out.println(date);                }            });        }        threadPool.shutdown();    }    public String date(int seconds) {        Date date = new Date(1000 * seconds);        String s = null;        synchronized (ThreadLocalDemo05.class) {            s = dateFormat.format(date);        }        return s;    }

这下好了,咱们加上 synchronized 是没有反复了,然而效率大大降低了

那么有没有什么既能够吃西瓜又能够捡芝麻的办法呢?

能够让每个线程都领有一个本人的 simpleDateFormat 对象来达到这个目标,这样就能两败俱伤了,说干就干

public class ThreadLocalDemo06 {    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);    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 ThreadLocalDemo06().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 new SimpleDateFormat("mm:ss");        }    };}

场景2

ok 场景2就是咱们目前我的项目中应用到的,利用 ThreadLocal 来控制数据权限

咱们想做到的是,每个线程内须要保留相似于全局变量的信息(例如在拦截器中获取的用户信息),能够让不同办法间接应用,防止参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保留一些业务内容,比方一个 UserRequest,这个 UserRequest中寄存一些这个用户的信息,诸如权限组、编号等信息

在线程生命周期内,都通过这个动态 ThreadLocal 实例的 get() 办法获得本人 set 过的那个对象,防止了将这个request作为参数传递的麻烦

于是咱们写了这样的一个工具类

public class AppUserContextUtil {    private static ThreadLocal<String> userRequest = new ThreadLocal<String>();    /**     * 获取userRequest     *     * @return     */    public static String getUserRequest() {        return userRequest.get();    }    /**     * 设置userRequest     *     * @param param     */    public static void setUserRequest(String param) {        userRequest.set(param);    }    /**     * 移除userRequest     */    public static void removeUserRequest() {        userRequest.remove();    }}

那么当一个申请进来的时候,一个线程会负责执行这个申请,无论这个申请经验过多少个类的办法的,都能够间接去 get 出咱们的 userRequest 从而进行业务解决或者权限管控

在 Thread 中如何存储

二话不说,上图

一个 Thread 外面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 外面却能够有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。

因为一个 Thread 是能够调用多个 ThreadLocal 的,所以 Thread 外部就采纳了 ThreadLocalMap 这样 Map 的数据结构来寄存 ThreadLocal 和 value。

咱们一起看下 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 抵触的时候,采纳的是拉链法。

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

应用姿态

Key透露

咱们方才介绍了 ThreadLocalMap,每一个 ThreadLocal 都有一个 ThreadLocalMap

只管咱们可能会这样操作 ThreadLocal instance = null ,将这个实例设置为 null,认为这样就能够居安思危了

然而,通过GC谨严的可达性的剖析,只管咱们在业务代码中把 ThreadLocal 实例置为了 null,然而在 Thread 类中仍然有这个援用链的存在。

GC 在垃圾回收的时候会进行可达性剖析,它会发现这个 ThreadLocal 对象仍然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存透露的状况。从而导致 OOM,从而导致中午告警,从而导致绩效325,从而辞职送外卖等等一系反馈

Doug Lea 思考到如此危险,所以 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 的内存泄露问题。

Value透露

咱们认真思考,ThreadLocalMap 的每个 Entry 都是一个对 key 的弱援用,然而这个 Entry 蕴含了一个对 value 的强援用

强援用那就意味着在线程生命不完结的时候,咱们这个变量永远存在咱们的内存里

然而很有可能咱们早就不须要这个变量了,Doug Lea 是个暖男,为咱们思考到了这个问题,在执行 ThreadLocal 的 set、remove、rehash 等办法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就能够被失常回收了。

然而假如 ThreadLocal 曾经不被应用了,那么实际上 set、remove、rehash 办法也不会被调用,与此同时,如果这个线程又始终存活、不终止的话,那么这个内存永远不会被GC掉,也就导致了 value 的内存透露,从而导致 OOM,从而导致中午告警,从而导致绩效325,从而辞职送外卖等等一系反馈

为了防止喜剧的产生,咱们在应用完了 ThreadLocal 之后,咱们应该手动去调用它的 remove 办法,目标是避免内存透露的产生。

public void remove() {    ThreadLocalMap m = getMap(Thread.currentThread());    if (m != null)        m.remove(this);}

remove 办法中,能够看出,它是先获取到 ThreadLocalMap 这个援用的,并且调用了它的 remove 办法。这里的 remove 办法能够把 key 所对应的 value 给清理掉,这样一来,value 就能够被 GC 回收了

小结

以上就是 《浅谈 ThreadLocal 的理论使用 》的全部内容了,在本文中咱们介绍了 ThreadLocal 的实用场景,并且针对场景进行了代码演示;意识了 ThreadLocal 在线程中到底是如何存储的;也学会了应用 ThreadLocal 的正确姿态。

如果本文对你有帮忙,欢送点赞、关注。