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

5次阅读

共计 5739 个字符,预计需要花费 15 分钟才能阅读完成。

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 的正确姿态。

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

正文完
 0