ThreadLocal 是啥

先来看看源码中关于ThreadLocal的注释:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code 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).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal<Integer> threadId =
 *         new ThreadLocal<Integer>() {
 *             @Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code 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).
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */

大致意思就是说,ThreadLocal用于在每个线程中都保存一个线程内部私有的值。即使是同一个ThreadLocal实例,在不同线程内部,其值也是相互独立的,即:即使在不同线程中操作同一个ThreadLocal实例,在不同线程中有不同的值且不会受到其他线程的影响。

可以将其看做是一种数据的“容器”,在该“容器”中保存的是同一种数据(初始值相同),但是对于不同的线程,其具体的值又互不干扰。

就好比,数据就是商店卖的同一种笔记本,线程就是不同的购买的人:

  • 刚刚买来时,本子都是一样的(初始值相同)
  • 但后续不同人在本子上都可以写不同的东西(不同线程可以对其进行不同操作)
  • 并且购买者也无法得知其他人在笔记本上写了啥、也不能去其他人的本子上写东西,不同人的笔记本并没有任何关联(不同线程读写都是独立的、互不干扰)

具体可以看下面的演示。

ThreadLocal 与普通变量的区别

直接看代码就可以知道其与普通变量的区别。

  1. 先定义一个包含普通变量和ThreadLocal变量用于测试的类

    class Test{
     // ThreadLocal 变量
     ThreadLocal x = new ThreadLocal(){
         @Override
         protected Object initialValue() {
             return Integer.valueOf(0);
         }
     };
    
     // 普通变量
     Integer y = 0;
    
     // 操作并打印 ThreadLocal 变量
     public void p1(){
         Integer val = (Integer) x.get();
         x.set(val+1);
         System.out.println(Thread.currentThread().getName()+" x:\t"+x.get());
     }
    
     // 操作并打印 普通变量
     public void p2(){
         synchronized (Test.class) {
             y++;
         }
         System.out.println(Thread.currentThread().getName()+" y:\t"+y);
     }
    }

    p1p2方法中,分别让ThreadLocal变量和普通变量加一,并打印线程名和对应值。

  2. 在main方法中操作

     public static void main(String[] args){
         Test t=  new Test();
         for (int i = 0; i < 3; i++) {
             Thread th = new Thread(){
                 @Override
                 public void run() {
                     for (int j = 0; j < 4; j++) {
                         t.p1();
                         t.p2();
                     }
                 }
             };
             th.start();
         }
     }

    main方法中,定义一个Test类型的变量t,再开启三个线程,每个线程都对同一个变量t执行4次对两种变量加一并打印的操作。

  3. 分析操作结果

    Thread-0 x:    1
    Thread-0 y:    1
    Thread-2 x:    1
    Thread-1 x:    1
    Thread-2 y:    2
    Thread-0 x:    2
    Thread-2 x:    2
    Thread-1 y:    3
    Thread-2 y:    5
    Thread-0 y:    4
    Thread-0 x:    3
    Thread-0 y:    6
    Thread-0 x:    4
    Thread-2 x:    3
    Thread-1 x:    2
    Thread-2 y:    8
    Thread-0 y:    7
    Thread-2 x:    4
    Thread-1 y:    9
    Thread-2 y:    10
    Thread-1 x:    3
    Thread-1 y:    11
    Thread-1 x:    4
    Thread-1 y:    12

    可以看到,在不同线程中,ThreadLocal型的变量x都是从1自增到4。而普通变量则是从1自增到了12

    普通的变量会受到所有线程的影响:每一个线程对其加一,一共执行了12次,所以值就成了12

    ThreadLocal变量则相对于每个线程独立:三个线程即使都是对t中的x执行加一操作,但是每个线程的操作中,都是从1自增到4,不同线程间并没有相互影响。

ThreadLocal 应用场景

ThreadLocal的特点是,每个线程的中ThreadLocal变量初始值是相同的、不同线程之间其值互相独立。

所以可以在不同线程中仅在初始值相同且不同线程不需要共享访问的变量中使用。

使用场景

  • 数据库连接池

  • Session管理

线程安全问题

涉及到线程安全的关键词:同步互斥
由于ThreadLocal作用是线程内部互相独立的值,所以不同线程访问到的值显然和其他线程无关,无法用于线程的同步或互斥。
ThreadLocal不能解决线程的安全问题。

ThreadLocal 底层实现原理

初始化

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

    setInitialValue方法中调用了initialValue方法用于获取初始值,而setInitialValue又是private的,所以无法在子类对其进行重写等操作。

  • initialValue() 返回用于初始化的初始值:

      protected T initialValue() {
          return null;
      }

    这个方法是protected的,一般通过重写这个方法,来返回初始值。用于在每个线程中对数据进行初始化。

    默认值为null,所以如果没有重写这个方法记得要对null值进行判断并处理。

设置值

  • 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 线程与值的映射:

      static class ThreadLocalMap {
    
          /**
           * The entries in this hash map extend WeakReference, using
           * its main ref field as the key (which is always a
           * ThreadLocal object).  Note that null keys (i.e. entry.get()
           * == null) mean that the key is no longer referenced, so the
           * entry can be expunged from table.  Such entries are referred to
           * as "stale entries" in the code that follows.
           */
          static class Entry extends WeakReference<ThreadLocal<?>> {
              /** The value associated with this ThreadLocal. */
              Object value;
    
              Entry(ThreadLocal<?> k, Object v) {
                  super(k);
                  value = v;
              }
          }
          // 省略部分代码
          …………
    }

    通过相关的注释可知,ThreadLocalMap内部的键值对是一个弱引用

获取值

  • get 获取值:
      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();
      }
    逻辑与set()类似,先后去当前线程,在通过当前线程查看映射中是否有对应值,有则返回,没有则返回默认的初始值。