本文已收录在Github关注我,紧跟本系列专栏文章,咱们下篇再续!

  • 🚀 魔都架构师 | 全网30W技术追随者
  • 🔧 大厂分布式系统/数据中台实战专家
  • 🏆 主导交易系统百万级流量调优 & 车联网平台架构
  • 🧠 AIGC应用开发先行者 | 区块链落地实践者
  • 🌍 以技术驱动创新,我们的征途是改变世界!
  • 👉 实战干货:编程严选网

1 线程封闭

多线程访问共享可变数据时,涉及线程间数据同步问题。但并非总是都要用到共享数据,线程封闭概念诞生。

数据都封闭在各自线程,天然的无需同步,这通过将数据封闭在线程中而避免同步的技术称为线程封闭。

避免并发异常,最简单的是线程封闭:把对象封装到一个线程里,只有该线程能看到此对象。

1.1 栈封闭

局部变量的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问该栈。

1.2 ThreadLocal实现线程封闭

ThreadLocal,Java的线程级变量,每个线程都有一个ThreadLocal,即每个线程都拥有自己独立的一个变量,竞争条件被彻底消除,在并发时是绝对安全的变量。

1.2.1 用法

ThreadLocal<T> var = new ThreadLocal<T>();

会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响。可以用 ThreadLocal 存储一些参数, 以便在线程中的多个方法中使用,代替方法传参。

1.2.2 实例

阿里三面:说说线程封闭与ThreadLocal的关系_多线程

ThreadLocal内部维护一个Map:

  • K=每个线程的名称
  • V=要封闭的对象

每个线程中的对象都对应着Map中的一个值,即ThreadLocal利用Map实现了对象的线程封闭。

2 游戏案例

CS游戏开始时,每个人一把枪,枪把上有三个数字:

  • 子弹数
  • 杀敌数
  • 自己的命数

初始值分别为1500、0、10。设战场上的每个人都是个线程,那么这三个初始值写在哪?若每个线程都写死这三个值,万一将初始子弹数统一改成1000发呢?

若共享,则线程之间的并发修改会导致数据不准确。能否构造这样一个对象,将这个对象设置为共享变量,统一设置初始值,但每个线程对该值的修改互相独立,这个对象就是ThreadLocal。

别翻译为线程本地化或本地线程,英语恰当的名称应该叫作:CopyValueIntoEveryThread。

2.1 代码

public class CsGameByThreadLocal {
  private static final Integer BULLET_NUMBER = 1500;
  private static final Integer KILLED_ENEMIES = 0;
  private static final Integer LIVE_VALUE = 10;
  private static final Integer TOTAL_PLAYERS = 10;
  /**
   * 随机数用来展示每个对象的不同的数据 (第1处)
   */
  private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();
  /**
   * 初始化子弹数
   */
  private static final ThreadLocal<Integer> BULLET_NUMBER_THREADLOCAL = ThreadLocal.withInitial(() -> BULLET_NUMBER);
  /**
   * 初始化杀敌数
   */
  private static final ThreadLocal<Integer> KILLED_ENEMIES_THREADLOCAL = ThreadLocal.withInitial(() -> KILLED_ENEMIES);
  /**
   * 初始化自己的命数
   */
  private static final ThreadLocal<Integer> LIVE_VALUE_THREADLOCAL = ThreadLocal.withInitial(() -> LIVE_VALUE);
  /**
   * 定义每位队员
   */
  private static class Player extends Thread {
      @Override
      public void run() {
          Integer bullets = BULLET_NUMBER_THREADLOCAL.get() - RANDOM.nextInt(BULLET_NUMBER);
          Integer killEnemies = KILLED_ENEMIES_THREADLOCAL.get() - RANDOM.nextInt( bound: TOTAL_PLAYERS / 2);
          Integer liveValue = LIVE_VALUE_THREADLOCAL.get() - RANDOM.nextInt(LIVE_VALUE);
          System.out.println(getName() + ", BULLET_NUMBER is " + bullets);
          System.out.println(getName() + ", KILLED_ENEMIES is " + killEnemies);
          System.out.println(getName() + ", LIVE_VALUE is " + liveValue + "\n");
          BULLET_NUMBER_THREADLOCAL.remove();
          KILLED_ENEMIES_THREADLOCAL.remove();
          LIVE_VALUE_THREADLOCAL.remove();
      }
  }
  public static void main(String[] args) {
      for (int i = 0; i < TOTAL_PLAYERS; i++) {
          new Player().start();
      }
  }
}

JVM维护了一个Map<Thread, T>,每个线程要用这个T时,用当前的线程去Map里面取。

该示例无 set 操作,那初始值咋进入每个线程,成为独立拷贝的?虽ThreadLocal在定义时重写initialValue(),但并非在BULLET_NUMBER_THREADLOCAL对象加载静态变量时执行。而是每个线程在ThreadLocal.get()时都会执行到:

2.2 get()

返回当前线程该线程局部变量副本中的值

public T get() {
  Thread t = Thread.currentThread();
  // 每个线程都有自己的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    // 若map已创建,表示Thread类的threadLocals属性已初始化完毕
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  // 若map ==null,则直接执行setInitialValue()
  // 即使map!=null,但若e==null,依然直接执行setinitialValue()
  return setInitialValue();
}

2.3 setinitialValue()

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

CsGameByThreadLocal中初始化ThreadLocal对象时已重写value = initialValue()。

getMap源码就是提取线程对象t的ThreadLocalMap属性: t. threadLocals.

CsGameByThreadLocal第1处,使用了ThreadLocalRandom 生成单独的Random实例; 该类在JDK7中引入,使得每个线程都有自己的随机数生成器。要避免Random实例被多线程使用,虽共享该实例是线程安全的,但会因竞争同一seed而导致性能下降。

ThreadLocal是每个线程单独持有的,因为每一个线程都有独立的变量副本,其他线程不能访问,所以不存在线程安全问题,也不会影响程序的执行性能。 ThreadLocal对象通常是由private static修饰的,因为都需要复制到本地线程,所以非static作用不大。 ThreadLocal无法解决共享对象的更新问题,下面的实例将证明这点: 因为CsGameByThreadLocal中使用的是Integer 不可变对象,所以可使用相同的编码方式来操作一下可变对象看看

import java.util.concurrent.TimeUnit;

public class InitValueInThreadLocal {
  private static final StringBuilder INIT_VALUE = new StringBuilder("init");
  // 重写 ThreadLocal 的 initialValue,返回 StringBuilder 静态引用
  private static final ThreadLocal<StringBuilder> builder
  = ThreadLocal.withInitial(() -> INIT_VALUE);

  private static class AppendStringThread extends Thread {
      @Override
      public void run() {
          StringBuilder inThread = builder.get();
          for (int i = 0; i < 10; i++) {
              inThread.append("-" + i);
              System.out.println(inThread.toString());
          }
      }
  }
  public static void main(String[] args) throws InterruptedException {
      for (int i = 0; i < 10; i++) {
          new AppendStringThread().start();
      }
      TimeUnit.SECONDS.sleep(timeout: 10);
  }
}

输出结果乱序不可控,所以使用某个引用来操作共享对象时,仍需线程同步:

阿里三面:说说线程封闭与ThreadLocal的关系_编程语言_02

ThreadLocal 有个静态内部类ThreadLocalMap,它还有一个静态内部类Entry; 在Thread中的ThreadLocalMap属性的赋值是在ThreadLocal类中的createMap.

ThreadLocal ThreadLocalMap有三组对应的方法: get()、set()和remove(); 在ThreadLocal中对它们只做校验和判断,最终的实现会落在ThreadLocalMap.. Entry继承自WeakReference,只有一个value成员变量,它的key是ThreadLocal对象

再从栈与堆的内存角度看看两者的关系

阿里三面:说说线程封闭与ThreadLocal的关系_编程语言_03

  • 一个Thread有且仅有一个ThreadLocalMap对象
  • 一个Entry对象的 key 弱引用指向一个ThreadLocal对象
  • 一个ThreadLocalMap 对象存储多个Entry 对象
  • 一个ThreadLocal 对象可被多个线程共享
  • ThreadLocal对象不持有Value,Value 由线程的Entry 对象持有

所有的Entry对象都被ThreadLocalMap类实例化对象threadLocals持有。

当线程执行完毕,线程内的实例属性均会被GC,弱引用的ThreadLocal,即使线程正在执行,只要ThreadLocal对象引用被置null,Entry的Key就会自动在下次YGC时被回收。

ThreadLocal使用set()/get()时,又会自动将那些key=null的value置null,使value能被GC,避免内存泄漏,现实很骨感,ThreadLocal如源码注释所述:

阿里三面:说说线程封闭与ThreadLocal的关系_Java并发编程_04

ThreadLocal对象通常作为private static变量使用,其生命周期至少不会随线程结束而结束。

2.4 API

set()

无该操作的ThreadLocal,易引起脏数据。设置此线程局部变量的当前线程的副本到指定的值,大多数的子类都无需重写此方法。

get()

始终无该操作的ThreadLocal对象,也就无意义!

remove()

没有该操作,易引起内存泄漏!

ThreadLocal是非static的,则属于某线程实例,就失去线程间共享的本质。ThreadLocal到底啥用?

  • 局部变量在方法内各代码块之间传递
  • 类变量在类内的方法之间传递

而复杂的线程方法可能需调用很多方法来实现某个功能,此时用啥传递线程内的变量?

ThreadLocal,通常用于同一个线程内的跨类、跨方法传递数据。

若无ThreadLocal,则相互之间的信息传递,势必要靠返回值和参数,无形之中,有些类甚至有些框架会耦合。而通过将Thread构造方法的最后一个参数设为true,可以把当前线程的变量继续往下传递给它创建的子线程:

public Thread (ThreadGroup group, Runnable target, String name,long stackSize, boolean inheritThreadLocals) [
   this (group, target, name,  stackSize, null, inheritThreadLocals) ;
}

parent为其父线程

if (inheritThreadLocals && parent. inheritableThreadLocals != null)
      this. inheritableThreadLocals = ThreadLocal. createInheritedMap (parent. inheritableThreadLocals) ;

createlnheritedMap()就是调用ThreadLocalMap的私有构造器产生一个实例对象,把父线程中不为null的线程变量都拷贝过来。

private ThreadLocalMap (ThreadLocalMap parentMap) {
    // table就是存储
    Entry[] parentTable = parentMap. table;
    int len = parentTable.length;
    setThreshold(len) ;
    table = new Entry[len];

    for (Entry e : parentTable) {
      if (e != null) {
        ThreadLocal<object> key = (ThreadLocal<object>) e.get() ;
        if (key != null) {
          object value = key. childValue(e.value) ;
          Entry c = new Entry(key, value) ;
          int h = key. threadLocalHashCode & (len - 1) ;
          while (table[h] != null)
            h = nextIndex(h, len) ;
          table[h] = C;
          size++;
        }
    }
}

很多case可通过ThreadLocal透传全局上下文。如用ThreadLocal存储监控系统的某标记位,命名traceld。某次请求下所有的traceld一致,以获得可统一解析的日志文件。

但实际开发,发现子线程里的traceld为null,跟主线程traceld不一致,就需要InheritableThreadLocal解决父子线程之间共享线程变量的问题,使整个连接过程中的traceld一致。

示例

public class RequestProcessTrace {

    private static final InheritableThreadLocal<FullLinkContext> FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL
            = new InheritableThreadLocal<FullLinkContext>();

    public static FullLinkContext getContext() {
        FullLinkContext fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        if (fullLinkContext == null) {
            FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.set(new FullLinkContext());
            fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        }
        return fullLinkContext;
    }

    private static class FullLinkContext {
        private String traceId;

        public String getTraceId() {
            if (StringUtils.isEmpty(traceId)) {
                FrameWork.startTrace(null, "JavaEdge");
                traceId = FrameWork.getTraceId();
            }
            return traceId;
        }

        public void setTraceId(String traceId) {
            this.traceId = traceId;
        }
    }

}

使用ThreadLocalInheritableThreadLocal透传上下文时,注意线程间切换、异常传输时的处理,避免在传输过程中因处理不当而导致的上下文丢失。

SimpleDateFormat是非线程安全的类,定义为static,会有数据同步风险。SimpleDateFormat内部有个Calendar对象,在日期转字符串或字符串转日期的过程中,多线程共享时很可能产生错误。推荐使用 ThreadLocal,让每个线程单独拥有该对象:

private static final ThreadLocal<DateFormat> DATE_FORMAT_THREADLOCAL = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat(pattern: "yyyy-MM-dd"));

3 ThreadLocal的副作用

ThreadLocal会产生:

  • 脏数据
  • 内存泄漏

通常是在线程池的线程中使用ThreadLocal而导致,因为线程池有如下特点:

  • 线程复用
  • 内存常驻

3.1 脏数据(用户信息错乱)

线程复用会产生脏数据。由于线程池会复用 Thread 对象,与 Thread 绑定的静态属性 ThreadLocal 变量也会被复用。 若在实现的线程run()方法中不显式调用remove(),清理与线程相关的ThreadLocal信息,那么若下一个线程不调用set(),就可能get() 到复用的线程信息。包括ThreadLocal所关联的线程对象的value值。

用户A下单后没有看到订单记录,用户B却看到A的订单记录。原请求过程中,用户每次请求Server,都得通过 sessionId 去缓存里查询用户的session信息,这无疑增加一次调用。

遇到采用某框架缓存每个用户对应的SecurityContext,其封装了session 相关信息。优化后,虽会为每个用户新建一个 session 相关上下文,但由于Threadlocal没有在线程处理结束时及时remove()。高并发case,线程池中的线程可能会读取到上一个线程缓存的用户信息。

/**
 * @author JavaEdge
 * @date 2022/10/4
 */
public class DirtyDataInThreadLocal {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(1);
        for (int i = 0; i < 2; i++) {
            MyThread myThread = new MyThread();
            threadPool.execute(myThread);
        }
    }

    private static class MyThread extends Thread {

        private static boolean flag = true;

        @Override
        public void run() {
            if (flag) {
                threadLocal.set(this.getName() + ", session info.");
                flag = false;
            }
            System.out.println(this.getName() + " 线程是 " + threadLocal.get());
        }
    }
}

结果:

Thread-0 线程是 Thread-0, session info.
Thread-1 线程是 Thread-0, session info.

再看个类似的案例。ThreadLocal适用于变量在线程间隔离,而在方法或类间共享。

若用户信息获取比较昂贵(如从DB查询),则在ThreadLocal中缓存比较合适。

使用ThreadLocal存放一个Integer值,代表需要在线程中保存的用户信息,初始为null。

先从ThreadLocal获取一次值,然后把外部传入的参数设置到ThreadLocal中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。

public Map wrong(@RequestParam("userId") Integer userId) {
    // 设置用户信息前,先查询一次ThreadLocal中的用户信息
    String before = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
    // 设置用户信息到ThreadLocal
    CURRENT_USER.set(userId);
    // 设置用户信息后,再查询一次ThreadLocal中的用户信息
    String after = Thread.currentThread().getName() + ":" + CURRENT_USER.get();
    // 聚合两次查询结果
    Map result = new HashMap();
    result.put("before", before);
    result.put("after", after);
    return result;
}

固定思维认为,在设置用户信息前第一次获取的值始终是null,但要清楚程序运行在Tomcat,执行程序的线程是Tomcat的工作线程,其基于线程池。

而线程池会复用固定线程,一旦线程复用,则可能首次从ThreadLocal获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal中的用户信息就是其他用户的信息。

bug复现

在配置文件设置Tomcat参数-工作线程池最大线程数设为1,这样始终是同一线程在处理请求:

server.tomcat.max-threads=1

user1请求接口,第1、2次获取到用户ID分别null和1,符合预期:

localhost:45678/threadlocal/wrong?userId=1

{
  "before": "http-nio-45678-exec-8:null",
  "after": "http-nio-45678-exec-8:1"
}

user2请求接口,bug复现!第1、2次获取到用户ID分别1、2,显然第一次获取到user1的信息,因为Tomcat线程池重用了线程。两次请求线程都是同一线程:http-nio-45678-exec-1

localhost:45678/threadlocal/wrong?userId=2
  
{
  "before": "http-nio-45678-exec-1:1",
  "after": "http-nio-45678-exec-1:2"
}

先搞清代码会跑在啥线程:

  • Tomcat下run的业务代码,本运行在一个多线程环境,不能认为没显式开启多线程,就无线程安全问题
  • 线程创建较昂贵,所以Web服务器会用线程池处理请求,线程会复用。使用ThreadLocal存放数据时,注意在代码运行完后,显式清空设置的数据

解决方案

在finally代码块显式清除ThreadLocal中数据。即使新请求过来,使用了之前的线程,也不会获取到错误的用户信息。

修正后:

@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
    String before = Thread.currentThread().getName() + ":" + currentUser.get();
    currentUser.set(userId);
    try {
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;
    } finally {
        // 在finally代码块中删除ThreadLocal中的数据,确保数据不串
        currentUser.remove();
    }
}

ThreadLocal利用独占资源解决线程安全问题,若就是要资源在线程间共享怎么办?就需要用到线程安全容器。

ThreadLocalRandom可将其实例设置到静态变量,在多线程下复用吗?

current()时初始化一个初始化种子到线程,每次nextseed再使用之前的种子生成新的种子:

UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);

若通过主线程调用一次current生成一个ThreadLocalRandom实例保存,那么其它线程来获取种子的时候必然取不到初始种子,必须是每一个线程自己用的时候初始化一个种子到线程。 可以在nextSeed设置一个断点看看:

UNSAFE.getLong(Thread.currentThread(),SEED);

3.2 内存泄漏

源码提示用static修饰ThreadLocal,在此场景下,寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的Value就不现实。

上例中,若不执行remove(),则当该线程执行完成后,通过ThreadLocal对象持有的String对象是不会被释放。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

用ThreadLocal保存一个V时,会在ThreadLocalMap中的数组插入一个Entry对象,按理KV都应以强引用保存在Entry对象,但ThreadLocalMap实现中,K被保存到WeakReference对象。

这导致ThreadLocal在没有外部强引用时,发生GC时就会被回收,若创建ThreadLocal的线程一直持续运行,则该Entry对象中的V就可能一直得不到回收。

应对策略

在调用ThreadLocal#get()、set()可能会清除ThreadLocalMap中K为null的Entry对象,这样对应的V就没有GC Roots可达,下次GC时就能被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

若使用ThreadLocal#set后,没有显式调用remove,就可能内存泄露。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("JavaEdge");
    // 其它业务逻辑
} finally {
    localName.remove();
}

以上两个问题的解决办法都简单:每次用完ThreadLocal时,及时调用remove()清理

4 原理

该类提供线程局部(thread-local)变量,这些变量不同于它们的普通对应物,因为访问某变量(通过其get/set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。

ThreadLocal实例通常是类中的 private static 字段,希望将状态与某一个线程(如用户ID或事务ID)相关联。

一个以ThreadLocal对象为K、任意对象为V的存储结构,类似HashMap,可保存KV对,但一个ThreadLocal只能保存一个KV对,各线程的数据互不干扰。该结构被附带在线程上,即一个线程可根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

ThreadLocal<String> localName = new ThreadLocal();
localName.set("JavaEdge");
String name = localName.get();
  • 在线程A中初始化了一个ThreadLocal对象localName,并set一个值JavaEdge。同时在线程A中通过get可拿到之前设置的值
  • 但若在线程B中,拿到的将是一个null

因为ThreadLocal保证了各个线程的数据互不干扰。

public class ThreadLocal<T> {
  
   /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
public class Thread implements Runnable {
  
  // 与当前线程关联的 ThreadLocal 值。该map由 ThreadLocal 类维护
  ThreadLocal.ThreadLocalMap threadLocals = null;

每个Thread中都有个ThreadLocalMap。

4.1 ThreadLocalMap

  • 执行set,其值保存在当前线程的threadLocals
  • 执行get,从当前线程的threadLocals域获取

所以在线程A中set的值,线程B永得不到。即使在线程B中重新set,也不影响A中的值, 保证了线程之间不会相互干扰。

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
      ...
    }

    private Entry[] table;
    ...
}

4.1.1 结构

类似HashMap,但在ThreadLocal中,并未实现Map接口。也是初始化一个大小为16的Entry数组:

private static final int INITIAL_CAPACITY = 16;

// 节点对象数组
private Entry[] table;

Entry保存每个KV对:

/**
 * 该哈希表中的条目继承自 WeakReference,并使用其主 ref 字段作为键 ( 该键始终是一个 ThreadLocal 对象 )。注意键为 null(即 entry.get() == null)表示该键已不再被引用,此时可以将相应的条目从表中移除。这类条目在后续代码中称为 “stale entries”(陈旧条目)。
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
  // 和该ThreadLocal关联的值
  Object value;

  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}

k恒为ThreadLocal,通过ThreadLocal#set(),把ThreadLocal对象自身当做key,放进ThreadLoalMap:

阿里三面:说说线程封闭与ThreadLocal的关系_多线程_05

ThreadLoalMapEntry继承WeakReference,Entry没有next字段,所以不存在链表case。

4.1.2 hash冲突

无链表,那hash冲突咋办?ThreadLocal 依赖附加到每个线程(Thread.threadLocals和InheritableThreadLocals)的线程线性探测哈希表。

threadLocalHashCode

ThreadLocal对象充当key,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在ThreadLocalMaps 中有用),它消除了在相同线程使用连续构造的threadlocal的常见情况下的冲突,而在不太常见情况仍表现良好。

ThreadLocal 通过这 hashCode,计算当前 ThreadLocal 在 ThreadLocalMap 中的索引

每个ThreadLocal对象都有个hash值:

// 即将分配的下一个ThreadLocal实例的threadLocalHashCode 的值
private final int threadLocalHashCode = nextHashCode();

每初始化一个ThreadLocal对象,hash值就增加一个固定大小:

private static AtomicInteger nextHashCode = new AtomicInteger();

// 连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
  return nextHashCode.getAndAdd(HASH_INCREMENT);
}

连续生成的哈希码之间的差值,该值设定参考ThreadLocal的hash算法(关于 0x61c88647)

注意这几个字段都是static修饰。ThreadLocalMap 会被 set 多个 ThreadLocal ,而多个 ThreadLocal 就根据 threadLocalHashCode 区分。

ThreadLoalMap#set
private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  // 根据ThreadLocal对象的hash值
  // 定位至table中的位置i
  int i = key.threadLocalHashCode & (len-1);

  // 位置i已有对象
  //  若和即将设置的K无关,则寻找下一空位
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

    //  若该Entry对象的key正是将设置的k
    if (k == key) {
      // 覆盖其value(和HashMap处理相同)
      e.value = value;
      return;
    }

    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
  // 若当前位置为空,就初始化一个Entry对象置于i
  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}
ThreadLoalMap#getEntry
private Entry getEntry(ThreadLocal<?> key) {
    // 也根据ThreadLocal#threadLocalHashCode
    // 定位到table中的位置
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 判断该位置Entry对象中的key是否和get的key一致
    if (e != null && e.get() == key)
        return e;
    else
        // 若不一致,就判断下一个位置
        return getEntryAfterMiss(key, i, e);
}

可见,set和get若冲突严重,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。

5 FAQ

ThreadLocal不是用来解决共享对象的多线程访问问题的,一般通过set()到线程中的对象是该线程自己使用的对象,其他线程无需访问的,也访问不到。各线程中访问的是不同对象。

ThreadLocal使各线程能保持各自独立的一个对象,并非通过set()实现,而是通过每个线程中的new对象的操作来创建的对象,每个线程创建一个,不是什么对象拷贝或副本。

  • 通过set()将这个新【创建的对象的引用】保存到各线程的自己的一个map,每个线程都有这样一个map
  • get()时,各线程从自己的map中取出放入的对象,因此取出来的是各自线程中的对象
  • ThreadLocal实例就作为map的K

注意,若:

  • set()进去的本就是多个线程共享的同一对象
  • 则多个线程的get()取得的还是这个共享对象本身

还是有并发访问问题。

避免共享变量的解决方案

高并发下,使用局部变量会频繁创建对象,使用threadlocal也是针对线程创建新变量,都是针对线程维度,threadlocal并未体现出优势,为什么还用threadlocal?

  • threadlocal=线程数
  • 局部变量=调用量

差距太大啦!

6 适用场景

6.1 Hibernate应用

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {
     // 先判断当前线程中是否放入session
        if (s == null) {
           // 若无,则创建一个session
            s = getSessionFactory().openSession();
           // 再将session set()到线程中,实际是放到当前线程的ThreadLocalMap
            threadSession.set(s);
       // 这时,对该session的唯一引用就是当前线程中的那个ThreadLocalMap 
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}

threadSession 作为这个值的K,要取得这个 session,可通过threadSession.get()。里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这session相当于线程的私有变量,而非public。

显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。要是session是多个线程共享使用的,那还不乱套了.

若不用ThreadLocal,怎么实现?

可能就要在action中创建session,然后把session一个个传到service和dao中,够烦的。 或自己定义一个静态map,将当前thread作为key,创建的session作为值,put到map中,这也是一般人的想法。

但ThreadLocal的实现刚相反,它是在每个线程中有个map,而将ThreadLocal实例作为K,这样每个map中的条目很少,且当线程销毁时相应东西也一并销毁。

总之,ThreadLocal不是用来解决对象共享访问问题,而主要是提供保持对象的方法和避免参数传递的方便的对象访问方式。

  • 每个Thread中都有一个自己的ThreadLocalMap类对象:可将Thread自己的对象保持到其中,各管各的,Thread可正确访问到自己的对象
  • 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同Thread的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免将这个对象作为参数传递的麻烦

当然,若将本来线程共享的对象通过set()放到Thread中也可,可实现避免参数传递的访问方式。但注意get()到的是那同一个共享对象,并发访问问题要靠其他手段解决。但一般来说,线程共享的对象通过设置为某类的静态变量即可实现方便访问,似乎没必要放到Thread中。

6.2 应用场合

最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

ThreadLocal中的变量只有这3个int型:

private final int threadLocalHashCode = nextHashCode();  
private static AtomicInteger nextHashCode =
        new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

创建一个ThreadLocal实例即new ThreadLocal()时做了哪些操作,构造方法ThreadLocal()里唯一操作:

private final int threadLocalHashCode = nextHashCode();  

private static int nextHashCode() {
  return nextHashCode.getAndAdd(HASH_INCREMENT);
}

就是将ThreadLocal类的下一个hashCode值即nextHashCode值赋给实例的threadLocalHashCode,然后nextHashCode的值增加HASH_INCREMENT。

因此ThreadLocal实例的变量只有这个threadLocalHashCode,且final,区分不同的ThreadLocal实例。 ThreadLocal类主要作工具类,set()进去的对象放在哪呢?

看上面的set()方法,两句合并一下

ThreadLocalMap map = Thread.currentThread().threadLocals;

这个ThreadLocalMap 类是ThreadLocal中定义的内部类,但是它的实例却在Thread类

public class Thread implements Runnable {  
    ......  
  
    /* ThreadLocal values pertaining to this thread. This map is maintained 
     * by the ThreadLocal class. */  
    ThreadLocal.ThreadLocalMap threadLocals = null;    
    ......  
}
if (map != null)
    // 将该ThreadLocal实例作为key,要保持的对象作为值,设置到当前线程的ThreadLocalMap
    map.set(this, value);