小明所在的项目组(迭代组:一直在迭代的路上),经常会在已有接口的基础上开发一些小功能,并且前提是在保证现有用户的不受影响基础上迭代。功能迭代,在代码层面小明有1w种实现方法(吹牛的),一起来看看这次小明如何使用ThreadLocal优雅地完成本次迭代吧!

由于 ThreadLocal 支持范型,如 ThreadLocal< StringBuilder >,为表述方便,后文用 变量 代表 ThreadLocal 本身,而用 实例 代表具体类型(如 StringBuidler )的实例。

理解误区

写这篇文章之前,小明就在网上看了很多博客关于 ThreadLocal 的适用场景以及解决的问题,描述的都并不是很清楚,甚至误人子弟的。比如下面是常见对于 ThreadLocal的介绍(wrong

ThreadLocal为解决多线程程序的并发问题提供了一种新的思路;

ThreadLocal的目的是为了解决多线程访问资源时的共享问题。

在小明大量阅读和动手实验后得出结论:ThreadLocal 并不是像上面所说为了解决多线程 共享变量的问题。

正确理解

ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< StringBuilder >而言即为 StringBuilder 类型变量),在不同的 Thread 中有不同的副本(实际上是不同的实例):

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用;
  • 既然其它 Thread 不可访问,那就不存在多线程间共享的问题。

官方文档是这样描述的:

我看完之后,得出这样的结论

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都会被回收。

因此ThreadLocal 非常适用于这样的场景:每个线程需要自己独立的实例且该实例需要在多个方法中使用。当然,使用其它方式也可以实现同样的效果,但是看完这篇文章,你会发现 ThreadLocal 会让实现更简洁、更优雅!

ThreadLocal用法

实例代码

我们通过下面的代码,先做个示例,然后分析一下现象,得出一个结论:

public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {
int threadNum = 3;
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for (int i = 1; i <= threadNum; i++) {
new Thread(() -> {
for (int j = 0; j <= 2; j++) {
MyUtil.add(String.valueOf(j));
MyUtil.print();
}
MyUtil.set("hello world");
countDownLatch.countDown();
}, "thread - " + i).start();
}
countDownLatch.await();
} private static class MyUtil { public static void add(String newStr) {
StringBuilder str = StringBuilderUtil.stringBuilderThreadLocal.get();
StringBuilderUtil.stringBuilderThreadLocal.set(str.append(newStr));
} public static void print() {
System.out.printf("Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
Thread.currentThread().getName(),
StringBuilderUtil.stringBuilderThreadLocal.hashCode(),
StringBuilderUtil.stringBuilderThreadLocal.get().hashCode(),
StringBuilderUtil.stringBuilderThreadLocal.get().toString());
} public static void set(String words) {
StringBuilderUtil.stringBuilderThreadLocal.set(new StringBuilder(words));
System.out.printf("Set, Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
Thread.currentThread().getName(),
StringBuilderUtil.stringBuilderThreadLocal.hashCode(),
StringBuilderUtil.stringBuilderThreadLocal.get().hashCode(),
StringBuilderUtil.stringBuilderThreadLocal.get().toString());
}
} private static class StringBuilderUtil {
// ThreadLocal 变量通常被 private static 修饰
private static ThreadLocal<StringBuilder> stringBuilderThreadLocal = ThreadLocal.withInitial(() -> new StringBuilder());
} }

实例分析

ThreadLocal本身支持范型,比如该例使用了 StringBuilder 类型的 ThreadLocal 变量。可通过 ThreadLocal 的 get() 方法读取 StringBuidler 实例,也可通过 set(T t) 方法设置 StringBuilder。

tips:CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个场景:任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。下次,我们可以单独聊聊这一个功能。

点击运行,控制台输出结果

我们可以发现:

  • 每个线程访问的是同一个 ThreadLocal 变量,而通过 ThreadLocal 的 get() 方法拿到的是不同的 StringBuilder 实例;
  • 虽然从代码上都是对 StringBuilderUtil 类的静态 stringBuilderThreadLocal 字段进行 get() 得到 StringBuilder 实例并追加字符串,但是这并不会将所有线程追加的字符串都放进同一个 StringBuilder 中,而是每个线程将字符串追加进各自的 StringBuidler 实例内
  • 使用 set(T t) 方法后,ThreadLocal 变量所指向的 StringBuilder 实例被替换

ThreadLocal原理

方案一

我们大胆猜想一下,既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案是 ThreadLocal 维护一个 Map,Key 是当前线程,Value是ThreadLocal在当前线程内的实例。这样,线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。该方案如下图所示

这个方案可以满足上文提到的每个线程内部都有一个ThreadLocal 实例备份的要求。每个新线程访问该 ThreadLocal 时,都会向 Map 中添加一个新的映射,而当每个线程结束时再清除该线程对应的映射。But,这样就存在两个问题:

  • 开启线程与结束线程时我们都需要及时更新 Map,因此必需保证 Map 的线程安全。
  • 当线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。

线程安全问题是JDK 未采用该方案的一个主要原因。

方案二

上面这个方案,存在多线程访问同一个 Map时可能会出现的同步问题。如果该 Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,就不存在这个问题。该方案如下图所示。

该方案虽然没有锁的问题,但是由于每个线程在访问ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),就有可能会造成内存泄漏的问题。我们一起来看一下Jdk8是如何解决这个问题的。

ThreadLocal 在 JDK 8 中的实现

ThreadLocalMap与内存泄漏

在该方案中,Map 由 ThreadLocal 类的静态内部类 ThreadLocalMap 提供。该类的实例维护某个 ThreadLocal 与具体实例的映射。与 HashMap 不同的是,ThreadLocalMap 的每个 Entry 都是一个对 Key 的弱引用,这一点我们可以从super(k)可看出。另外,每个 Entry 中都包含了一个对 Value 的强引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

之所以使用弱引用,是因为当没有强引用指向 ThreadLocal 变量时,这个变量就可以被回收,就避免ThreadLocal 因为不能被回收而造成的内存泄漏的问题。

但是,这里又可能出现另外一种内存泄漏的问题。ThreadLocalMap 维护 ThreadLocal 变量与具体实例的映射,当 ThreadLocal 变量被回收后,该映射的键变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。

注意:Entry是对 ThreadLocal 类型的弱引用,并不是具体实例的弱引用,因此还存在具体实例相关的内存泄漏的问题。

读取实例

我们来看一下ThreadLocal获取实例的方法

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();
}

当线程获取实例时,首先会通过getMap(t)方法获取自身的 ThreadLocalMap。从如下该方法的定义可见,该 ThreadLocalMap 的实例是 Thread 类的一个字段,即由 Thread 维护 ThreadLocal 对象与具体实例的映射,这一点与上文分析一致。

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

获取到 ThreadLocalMap 后,通过map.getEntry(this)方法获取该 ThreadLocal 在当前线程的 ThreadLocalMap 中对应的 Entry。该方法中的 this 即当前访问的 ThreadLocal 对象。

如果获取到的 Entry 不为 null,从 Entry 中取出值即为所需访问的本线程对应的实例。如果获取到的 Entry 为 null,则通过setInitialValue()方法设置该 ThreadLocal 变量在该线程中对应的具体实例的初始值。

设置初始值

设置初始值方法如下

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;
}

该方法为 private 方法,无法被重载。

首先,通过initialValue()方法获取初始值。该方法为 public 方法,且默认返回 null。所以典型用法中常常重载该方法。上例中即在内部匿名类中将其重载。

然后拿到该线程对应的 ThreadLocalMap 对象,若该对象不为 null,则直接将该 ThreadLocal 对象与对应实例初始值的映射添加进该线程的 ThreadLocalMap中。若为 null,则先创建该 ThreadLocalMap 对象再将映射添加其中。

这里并不需要考虑 ThreadLocalMap 的线程安全问题。因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它,其它线程不会访问该 ThreadLocalMap,也即该对象不会在多个线程中共享,也就不存在线程安全的问题。

设置实例

除了通过initialValue()方法设置实例的初始值,还可通过 set 方法设置线程内实例的值,如下所示。

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 对象,然后直接将 ThreadLocal 对象(即代码中的 this)与目标实例的映射添加进 ThreadLocalMap 中。当然,如果映射已经存在,就直接覆盖。另外,如果获取到的 ThreadLocalMap 为 null,则先创建该 ThreadLocalMap 对象。

防止内存泄漏

对于已经不再被使用且已被回收的 ThreadLocal 对象,它在每个线程内对应的实例由于被线程的 ThreadLocalMap 的 Entry 强引用,无法被回收,可能会造成内存泄漏。

针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。

private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

案例

对于 Java Web 应用而言,Session 保存了很多信息。很多时候需要通过 Session 获取信息,有些时候又需要修改 Session 的信息。一方面,需要保证每个线程有自己单独的 Session 实例。另一方面,由于很多地方都需要操作 Session,存在多方法共享 Session 的需求。如果不使用 ThreadLocal,可以在每个线程内构建一个 Session实例,并将该实例在多个方法间传递,如下所示。

public class SessionHandler {

  @Data
public static class Session {
private String id;
private String user;
private String status;
} public Session createSession() {
return new Session();
} public String getUser(Session session) {
return session.getUser();
} public String getStatus(Session session) {
return session.getStatus();
} public void setStatus(Session session, String status) {
session.setStatus(status);
} public static void main(String[] args) {
new Thread(() -> {
SessionHandler handler = new SessionHandler();
Session session = handler.createSession();
handler.getStatus(session);
handler.getUser(session);
handler.setStatus(session, "close");
handler.getStatus(session);
}).start();
}
}

该方法是可以实现需求的。但是每个需要使用 Session 的地方,都需要显式传递 Session 对象,方法间耦合度较高,给人的感觉并不优雅。

这里使用 ThreadLocal 重新实现该功能如下所示。

public class SessionHandler {

  public static ThreadLocal<Session> session = ThreadLocal.<Session>withInitial(() -> new Session());

  @Data
public static class Session {
private String id;
private String user;
private String status;
} public String getUser() {
return session.get().getUser();
} public String getStatus() {
return session.get().getStatus();
} public void setStatus(String status) {
session.get().setStatus(status);
} public static void main(String[] args) {
new Thread(() -> {
SessionHandler handler = new SessionHandler();
handler.getStatus();
handler.getUser();
handler.setStatus("close");
handler.getStatus();
}).start();
}
}

可以看到,改造过后的代码,不再需要在各个方法间来回传递 Session 对象,并且不费吹灰之力保证了每个线程都能够拥有自己独立的实例。虽然单看其中某一点,备选方案还很多。比如还可以通过在线程内创建局部变量保证每个线程有自己的实例,通过静态变量可实现变量在方法间的共享。但如果还需要同时满足变量在线程间的隔离与方法间的共享,ThreadLocal再合适不过。

总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

    欢迎关注微信公众号”程序员小明”,获取更多资源。

ThreadLocal解决了什么问题的更多相关文章

  1. ThreadLocal解决线程安全问题

    一.线程安全问题产生的原因 线程安全问题都是由全局变量及静态变量引起的 二.线程安全问题 SimpleDateFormate sdf = new SimpleDateFormat();使用sdf.pa ...

  2. 【Java并发编程】12、ThreadLocal 解决SimpleDateFormat非线程安全

    大致意思:Tim Cull碰到一个SimpleDateFormat带来的严重的性能问题,该问题主要有SimpleDateFormat引发,创建一个 SimpleDateFormat实例的开销比较昂贵, ...

  3. ThreadLocal解决什么问题

    原创文章,转载请务必将下面这段话置于文章开头处(保留超链接).本文转发自技术世界,原文链接 http://www.jasongj.com/java/threadlocal/ ThreadLocal解决 ...

  4. ThreadLocal解决SimpleDateFormat多线程安全问题中遇到的困惑

    测试代码: public class Main { public static void main(String[] args) { for (int k = 0; k < 10; k++) { ...

  5. ThreadLocal 解决simpledateformat线程不安全

    SimpleDateFormat在多线程情况下会出现线程不安全的情况,故用ThreadLoacl 处理/** * 用ThreadLocal处理simplDateFormat线程不安全 */public ...

  6. 19.ThreadLocal方法解决代码不友好的问题

    #ThreadLocal import threading #创建全局ThreadLocal loacl_school = threading.local() class Student(): def ...

  7. 理解ThreadLocal(之一)

    ThreadLocal是什么 在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地编 ...

  8. 线程隔离ThreadLocal

    ThreadLocal是什么 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地 ...

  9. Java 线程 — ThreadLocal

    ThreadLocal 先来看看ThreadLocal的注释: This class provides** thread-local variables**. These variables diff ...

随机推荐

  1. table的列固定

    <body onload="showFix(true,false,initTableId);"> <!doctype html> <html lang ...

  2. 外行人都能看懂的WebFlux,错过了血亏!

    前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 本文知识点架构: 如果有关注我公众号文章的同学就会发 ...

  3. T-SQL, Part III: Check table's existance

    There are several approaches to achieve so. Just list out all approaches I have tried: Approach 1: s ...

  4. ios遇到的坑

    总结体会:很多ios兼容性问题都是由于body设置了height:100% ios中input输入不了 在ios中margin属性不起作用 设置html body的高度为百分比时,margin-bot ...

  5. iOS核心动画高级技巧-1

    1. 图层树 图层的树状结构 巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 Core Animation其实是一个令人误解的命名.你可能认为它只是用来做动画的,但实际上它是从一个叫做L ...

  6. 看淡生死,不服就干(C语言指针)

    看淡生死,不服就干 emmmmm 其实今天蛮烦的 高等数学考的一塌糊涂 会的不会的都没写 真心没有高中轻松了啊 也不知道自己立的flag还能不能实现 既然选择了就一定坚持下去啊 下面还是放一段之前写的 ...

  7. 软件测试从业者必备的高频Linux命令

    命令 cd 1.如何进入上级目录 cd .. 2.如何进入当前用户主目录 cd ~ 3.如何进入上两级目录 cd ../.. 4.进入当前目录命令 cd . 5.如何进入目录 /usr/isTeste ...

  8. H3C交换机console登录配置 v7

    一.通过con口只需输入password登陆交换机. [H3C]user-interface aux 0 设置认证方式为密码验证方式 [H3C-ui-aux0] authentication-mode ...

  9. Scala函数式编程(四)函数式的数据结构 上

    这次来说说函数式的数据结构是什么样子的,本章会先用一个list来举例子说明,最后给出一个Tree数据结构的练习,放在公众号里面,练习里面给出了基本的结构,但代码是空缺的需要补上,此外还有预留的test ...

  10. flask实现验证码并验证

    效果图: 点击图片.刷新页面.输入错误点击登录时都刷新验证码 实现步骤: 第一步:先定义获取验证码的接口 verificationCode.py #验证码 @api.route('/imgCode') ...