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 的正确姿态。
如果本文对你有帮忙,欢送点赞、关注。