ThreadLocal 的原理与适用场景

ThreadLocal 解决什么问题 ?

不恰当的理解

下面是网络上常见的ThreadLocal的介绍:

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

合理的理解

ThreadLocal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal<String>而言,即为String类型变量),在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个Thread 内有自己的实例副本,且该副本只能由当前Thread 使用。这也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其他 Thread 不可访问,那就不存在多线程共享问题。
  • 既无共享,何来同步资源,又何来解决同步问题一说?

那 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

核心意思是

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

总的来说,ThreadLocal 使用与每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也就变量在线程间隔离而在方法或类间共享的场景。

ThreadLocal 用法

实例代码
下面通过如下代码来说明ThreadLocal的使用方式:

** 实例分析**
ThreadLocal 本身支持泛型。该实例使用 StringBuffer 类型的 ThreadLocal 变量。可以通过 ThreadLocal 的 get() 方法读取StringBuffer 实例,也可通过 set(T t) 方法设置 StringBuffer实例。

上述代码执行结果如下:


从上面的输出可看出

从第1-3行输出可见,每个线程通过 ThreadLocal 的 get() 方法拿到的是不同的 StringBuilder 实例

  • 第1-3行输出表明,每个线程所访问到的是同一个 ThreadLocal 变量
  • 从7、12、13行输出以及第30行代码可见,虽然从代码上都是对 Counter 类的静态 counter 字段进行 get() 得到 StringBuilder 实例并追加字符串,但是这并不会将所有线程追加的字符串都放进同一个 StringBuilder 中,而是每个线程将字符串追加进各自的 StringBuidler 实例内
  • 对比第1行与第15行输出并结合第38行代码可知,使用 set(T t) 方法后,ThreadLocal 变量所指向的 StringBuilder 实例被替换

ThreadLocal 原理

ThreadLocal 维护线程与实例的映射

既然每个访问ThreadLocal 变量的线程都自己的一个 “本地” 实例副本。一个可能的方案是 ThreadLocal 维护一个Map,key 为 Thread,value 是它在该Thread 内的实例。线程通过该 ThreadLocal 的get方法获取实例时,只需要以线程为键,从Map中找到对应的实例即可。该方案如下图所示:

该方案可满足上文提到的每个线程内一个独立备份的需求。每个新线程访问该ThreadLocal时,需要向Map中添加一个映射,而每个线程结束时,应该清除该映射。这里就有两个问题:

  • 增加线程与减少线程均需要写map,故需保证该Map 线程安全。虽然 可通过ConcurrentHashMap等方式实现线程安全,但或多或少都需要锁来保证线程的安全性
  • 线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能引起内存泄露。

Thread 维护ThreadLocal与实例的映射

上述方案中,出现锁的问题,原因在于多线程访问同一个 Map。如果该Map 由 Thread 维护,从而使得每个Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案如下图所示:

该方案虽然没有锁的问题,但是由于每个线程访问某ThreadLocal 变量后,都会在自己的Map 内维护该ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄露。

ThreadLocal 在JDK 8 中的实现

ThreadLocalMap 与内存泄漏

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

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类型的弱引用(也即上文所述它是对 键 的弱引用),而非具体实例的弱引用,所以无法避免具体的实例相关的内存泄漏。

读取实例

读取实例方法源码如下:

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 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。

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

使用场景

如上文所述,ThreadLocal 适用于如下两种场景:

  • 每个线程需要有自己的单独实例
  • 实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程都拥有自己的实例,实现方式有很多。例如,可以在线程内部构建一个单独的实例。ThreadLocal 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

案例

对应JavaWeb 应用而言,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();
  }
}

使用 ThreadLocal 改造后的代码,不在需要各个方法间传递 Session 对象,并且也非常轻松的保证了每个线程拥有自己独立的session实例。

如果单看其中某一点,替代方法有很多。比如可以通过线程内创建局部变量可实现每个线程有自己的实例,使用静态变量可实现变量在方法间共享。但如果同时满足变量在线程间隔离且方法共享,ThreadLocal再适合不过。

总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个 Map(ThreadLocalMap)并维护了 ThreadLocal 对象与具体实例的映射,该Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题。
  • ThreadLocalMap 的 Entry 对ThreadLocal 的引用为弱引用,避免了ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set方法通过调用 replaceStaleEntry 方法回收键为null 的 Entry 对象的值(即具体实例)以及Entry 对象本身,从而防止内存泄漏
  • ThreadLocal 使用于变量在线程间隔离且在方法间共享的场景
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容