关于java:浅谈-ThreadLocal-的实际运用

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:05
00:07
00:05
00:05
00:06
00:05
00:05
00:11
00:05
00:12
00: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 的正确姿态。

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理