ThreadLocal顾名思义,就是每个线程保存一份只有自己能访问的对象,避免了不同线程对同一个线程的竞争。所以核心在于如何隐藏一个对象的可见性,来保证只有指定的线程能访问到。反而言之,如果把ThreadLocal控制的对象发布出去,那么此对象也不再是线程安全的。
设计要点就是
1.不会意外的被不同线程共享。
2.合理的资源回收时机。
应用场景:
1.池化技术,先从自己线程中找对象,然后再去公共的找,避免竞争。
2.不同线程间传递技术,tomcat
3.局部变量跨方法传递
ThreadLocal本质上是一个key,不同线程可以用相同的key给自己保存不同的对象。作为一个key,生命周期就可以是很长的,类比我们经常用作key的String对象。
所以保存不同的对象,需要的是不同的key,也就创建新的ThreadLocal。
和普通的Key不同的是,这是个能get和set的key。后续我们看这个特殊的key是怎么实现的。下面是最基本的用法:
public class main {
    private static ThreadLocal<String> sThreadLocal = new ThreadLocal<>();
    public static void main(String args[]) {
        sThreadLocal.set("这是在主线程中");
        System.out.println("线程名字:" + Thread.currentThread().getName() + "---" + sThreadLocal.get());
        //线程a
        new Thread(new Runnable() {
            @Override
            public void run() {
                sThreadLocal.set("这是在线程a中");
                System.out.println("线程名字:" + Thread.currentThread().getName() + "---" + sThreadLocal.get());
            }
        }, "线程a").start();
    }
}
那同一个ThreadLocal怎么和不同的Thread关联呢?用的是Thread.currentThread()。这个方法看似是一个静态方法,但是返回的Thread对象是每个线程都不一样的,所以可以通过这个核心方法来关联。 不同线程的多个对象,用同一个ThreadLocal来作为对外的公开key表示。
class Thread {
  ThreadLocal.ThreadLocalMap threadLocals;  //作为线程类的实例变量,所以每个线程实例有一个自己的map
}
value的生命周期的控制
如果Thread声明周期过长,线程被复用,还要考虑value的生命周期问题。
value的生命周期可以通过控制ThreadLocal对象的生命周期来表示。比如把ThreadLocal设置为局部变量或者是静态变量。同时,因为ThreadLocal对象设置为局部变量被回收,且忘记通过代码ThreadLocal提前释放value,导致我们后续无法访问ThreadLocal去处理释放内存,就会导致内存泄漏。简而言之,TheadLocalMap没有了key但是value还存在,这通常意味着内存泄漏。所以一定要有一个机制去跟踪ThreadLocal对象被回收这个事件,也就是通过弱引用。弱引用可以视作一种事件通知机制。
下面来看这个ThreadLocalMap的set方法是怎么实现的。可以看到数据是保存在Entry数组里。
从key和value的角度来说,key是可以线程间共享的,而保存value的数据结构ThreadLocalMap则是每个线程自留的。

可以看到 super(k) 这一行,使用WeakReference引用ThreadLocal对象。value是强引用。Entry通过继承完成了,一个弱key,强value的结构。

Key的弱引用来实现自动清理
ThreadLocalMap本身并没有为外界提供取出和存放数据的API,我们所能获得数据的方式只有通过ThreadLocal类提供的get和set来间接的从ThreadLocalMap取出数据,一旦我们失去了对ThreadLocal的访问权,且对应的value没有被回收,还是被ThreadLocalMap强引用的时候,那就有可能造成内存泄漏。
ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。但key什么时候为null呢?理论上除了无用,key不应该为null的。误用就是指把key作为临时变量使用了。但是key作为临时变量的话,就不能跨方法访问了,对ThreadLocal的使用场景之一就是跨方法,跨类去访问一个对象。
所以这个通常是作为误用情况下的一种防呆机制设计。
正常使用的情况下,是长生命周期比如静态的key,配合短生命周期的线程。这样value就会随着线程消失而消失。
但如果线程也是长生命周期的话,就会出现value生命周期过长的问题。此时置空静态key也不对,因为key本身只是一个标志,而且只有一份,不会占很多内存。但是value确实很多份的,也可能是个很大的对象。
至于ThreadLocalMap为什么不直接提供给外界,我觉得还是为了防止意外共享。否则就会变成:
    private static String key = "foo"
    Thread.currentThread().threadLocalMap.get(key)
    Thread.currentThread().threadLocalMap.set(key)
如果 value 也是弱引用:
threadLocalMap作为保存对象的容器,一定会持有一份key和value的引用。
key是对外操作的窗口,所以除了容器保存一份外,外部也会保存一份引用。而value是被使用的,所以可以只需要容器一份就够了。
当容器对value也是弱引用持有,也就是没有任何强引用指向 value 时,value 会被 GC 回收。出现的问题就是ThreadLocal 对象可能仍然有效,但 ThreadLocal.get() 因为value被回收突然返回 null,违背预期行为。
这种不可预测的行为会破坏程序的正确性。
正确性就是 key在的时候,value一定要在。key不在的时候,为了内存不泄露,value也应该不在。但是不强调value和key同时不在。
- 明确的生命周期管理
 Key 的生命周期:由外部控制(开发者是否持有 ThreadLocal 实例的强引用)
 Value 的生命周期:应与所属线程绑定,不应仅由引用强度决定
 如果 value 是弱引用,线程还在运行但 value 被回收,会导致逻辑错误
一些常见疑惑解答:
为什么用Entry数组而不是Entry对象
ThreadLocalMap内部的table为什么是数组而不是单个对象呢?
答:因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocal,ThreadLocalMap在一个线程里就一个,因为ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。
ThreadLocal里的对象一定是线程安全的吗
未必,如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()获取的还是这个共享对象本身,还是有并发访问线程不安全问题。
ThreadLocal的使用示例
如果让我们自己来实现一个数据库连接池,最简单的办法就是用两个阻塞队列来实现,一个用于保存空闲数据库连接的队列 idle,另一个用于保存忙碌数据库连接的队列 busy;获取连接时将空闲的数据库连接从 idle 队列移动到 busy 队列,而关闭连接时将数据库连接从 busy 移动到 idle。
// 忙碌队列
BlockingQueue<Connection> busy;
// 空闲队列
BlockingQueue<Connection> idle;
这种方案将并发问题委托给了阻塞队列,实现简单,但是性能并不是很理想。因为 Java SDK 中的阻塞队列是用锁实现的,而高并发场景下锁的争用对性能影响很大。而 ConcurrentBag 通过 ThreadLocal 做一次预分配,避免直接竞争共享资源,非常适合池化资源的分配。需要开一个单独的线程池。
SynchronousQueue 主要用于线程之间传递数据。
// 用于存储所有的数据库连接
CopyOnWriteArrayList<T> sharedList;
// 线程本地存储中的数据库连接
ThreadLocal<List<Object>> threadList;
// 等待数据库连接的线程数
AtomicInteger waiters;
// 分配数据库连接的工具
SynchronousQueue<T> handoffQueue;
当线程池创建了一个数据库连接时,通过调用 ConcurrentBag 的 add() 方法加入到 ConcurrentBag 中,下面是 add() 方法的具体实现,逻辑很简单,就是将这个连接加入到共享队列 sharedList 中,如果此时有线程在等待数据库连接,那么就通过 handoffQueue 将这个连接分配给等待的线程。
// 将空闲连接添加到队列
void add(final T bagEntry){
  // 加入共享队列
  sharedList.add(bagEntry);
  // 如果有等待连接的线程,
  // 则通过 handoffQueue 直接分配给等待的线程
  while (waiters.get() > 0
    && bagEntry.getState() == STATE_NOT_IN_USE
    && !handoffQueue.offer(bagEntry)) {
      yield();
  }
}
通过 ConcurrentBag 提供的 borrow() 方法,可以获取一个空闲的数据库连接,borrow() 的主要逻辑是:
- 首先查看线程本地存储是否有空闲连接,如果有,则返回一个空闲的连接;
- 如果线程本地存储中无空闲连接,则从共享队列中获取。
- 如果共享队列中也没有空闲的连接,则请求线程需要等待。
 需要注意的是,线程本地存储中的连接是可以被其他线程窃取的,所以需要用 CAS 方法防止重复分配。在共享队列中获取空闲连接,也采用了 CAS 方法防止重复分配。sharedlist和其他线程的threadlocal里有可能都有同一个连接,从前者取到连接,就相当于窃取了其他线程的threadLocal里的链接
T borrow(long timeout, final TimeUnit timeUnit){
  // 先查看线程本地存储是否有空闲连接
  final List<Object> list = threadList.get();
  for (int i = list.size() - 1; i >= 0; i--) {
    final Object entry = list.remove(i);
    final T bagEntry = weakThreadLocals
      ? ((WeakReference<T>) entry).get()
      : (T) entry;
    // 线程本地存储中的连接也可以被窃取,
    // 所以需要用 CAS 方法防止重复分配
    if (bagEntry != null
      && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
      return bagEntry;
    }
  }
  // 线程本地存储中无空闲连接,则从共享队列中获取
  final int waiting = waiters.incrementAndGet();
  try {
    for (T bagEntry : sharedList) {
      // 如果共享队列中有空闲连接,则返回
      if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
        return bagEntry;
      }
    }
    // 共享队列中没有连接,则需要等待
    timeout = timeUnit.toNanos(timeout);
    do {
      final long start = currentTime();
      final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
      if (bagEntry == null
        || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
          return bagEntry;
      }
      // 重新计算等待时间
      timeout -= elapsedNanos(start);
    } while (timeout > 10_000);
    // 超时没有获取到连接,返回 null
    return null;
  } finally {
    waiters.decrementAndGet();
  }
}