# 二、ThreadLocal

# 1. 两大使用场景

# 1.1 场景一:initialValue

每个线程都需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random)。

image-20210317202704295
  • 每个 Thread 内有自己的实例副本,不共享。
  • 线程安全,因为每个线程都是用自己独享的 SimpleDateFormat
  • 只创建了 10 个 SimpleDateFormat 对象,这样就不用创建 1000 个,减少了内存损耗。
  • 不需要加锁,效率高。
public class ThreadLocalCaseOne{
		//线程池
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    /**
     * 利用传进来的秒数生成一个时间
     *
     * @param seconds
     * @return
     */
    public String date(int seconds){
        //获取 ThreadLocal 中的 SimpleDateFormat
        SimpleDateFormat sdf = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return sdf.format(new Date(seconds * 1000));
    }


    public static void main(String[] args) {
        //1000个打印日期的任务,用线程池
        for (int i = 0; i < 1000; i++) {
            int second = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(new ThreadLocalCaseOne().date(second));
                }
            });
        }
    }
}

// ThreadLocal
class ThreadSafeFormatter{
    //每个线程都有自己单独一份 SimpleDateFormat
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        //※ 重写 initialValue() 方法
      	@Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
}

# 1.2 场景二:set

同一次请求(同一个线程)内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。

image-20210317202621510
  • 无需 synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可以达到保证当前线程获取对应的用户信息的目的。
  • 每个线程独享自己的 User 对象,同一个线程之间维护一个上下文(Holder),方便信息传递。
/**
 * 利用 ThreadLocal 来实现同一个线程内不同方法的参数传递
 *
 * @author Hedon Wang
 * @create 2021-03-17 8:25 PM
 */
public class ThreadLocalCaseTwo {

    public static void main(String[] args) {
        new Service1().process();
    }

}

/**
 * 第一层处理
 */
class Service1{

    public void process(){
        User user = new User("Hedon");
        //放入用户信息上下文
        UserContextHolder.threadLocal.set(user);
        //往后调用
        new Service2().process();
    }
}

/**
 * 第二层处理
 */
class Service2{

    public void process(){
        //获取用户上下文中用户信息
        User user = UserContextHolder.threadLocal.get();
        System.out.println("service2 获得的用户信息:" + user);
        //往后调用
        new Service3().process();
    }

}

/**
 * 第三层处理
 */
class Service3{

    public void process(){
        //获取用户上下文中用户信息
        User user = UserContextHolder.threadLocal.get();
        System.out.println("service3 获得的用户信息:" + user);
    }

}

/**
 * 用户信息上下文:ThreadLocal
 */
class UserContextHolder{
    public static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}

/**
 * 用户类
 */
class User{
    String name;
    public User(String name){
        this.name = name;
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

结果:与 Service1 同属一个线程的 Service2 和 Service3 都可以拿到 Service1 放进去的 User 信息。

image-20210317204758140 image-20210317204739555

对比:

场景一:initialValue 场景二:set
在 ThreadLocal 第一个 get 的时候把对象初始化出来,对象的初始化时机可以由我们控制。 如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用 ThreadLocal.set 直接放到我们的 ThreadLocal 中,以便我们后续使用。

# 2. 作用

  1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象),且在任何方法中可以轻松地获取该对象。
  2. 线程安全。
  3. 不需要加锁,提高执行效率。
  4. 更高效地利用内存、节省开销。
  5. 免去传参的繁琐,降低代码耦合度。

# 3. 源码

image-20210317205552632
  • 一个 Thread 对应一个 ThreadLocalMap。
  • 一个 ThreadLocalMap 存储很多个 ThreadLocal。
  • ThreadLocal 其实就一个 key-value 键值对,key 是 ThreadLocal,value 是任何对象。

# 3.1 T initialValue()

private T setInitialValue() {
  	//调用 initialValue() 方法
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
      	//key:this(ThreadLocal),value: T(initialValue 中创建的对象)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
protected T initialValue() {
  	//默认返回 null
    return null;
}
  • 因为 initialValue() 默认返回一个 null,所以我们上述场景一需要重写 initialValue() 方法,让它来为我们生成我们想要的对象。

其他注意点:

  1. 该方法回返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用 get() 的时候,才会触发。
  2. 当线程第一次使用 get() 方法访问变量时,将调用次方法,除非线程先调用了 set() 方法,在这种情况下,不会为线程调用本 initialValue() 方法。
  3. 通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后(ThreadLocalMap 没了),再调用 get(),则可以再次调用此方法。

# 3.2 set()

public void set(T value) {
  	//1. 获取当前线程
    Thread t = Thread.currentThread();
  	//2. 得到当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
      	//4. 之后 set 的时候就把 value 放入 map 中
        map.set(this, value);
    else
      	//3. 第一次调用为 map 为空,所以会创建一个 map
        createMap(t, value);
}

# 3.3 get()

public T get() {
  	//1. 获取当前线程
    Thread t = Thread.currentThread();
  	//2. 得到当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
  	//3-1 如果之前有调用过 set(),那 map 就不为空,就执行下面的 if 语句
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
  	//3-2 如果之前没有调用过 set(),那就会调用 setInitialValue(),而 setInitialValue() 里面又调用了 initialValue()
    return setInitialValue();
}

# 3.4 remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
  	//删除 map
    if (m != null)
        m.remove(this);
}

ThreadLocalMap.remove()

/**
 * Remove the entry for key.
 */
private void remove(ThreadLocal<?> key) {
    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)]) {
      	//找到对应的 key (threadLocal) 的 value,然后删除它们
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

# 3.5 ThreadLocalMap

  • Hash 冲突的时候不采用拉链法,而且继续往后找空位。
static class ThreadLocalMap {

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

# 4. 注意点

# 4.1 内存泄漏

  • 正常情况下,当线程终止时,保存在 ThreadLocal 里的 value 会被 GC,因为没有任务强引用了。

  • 但是如果使用线程池的话,线程不会终止,那么就存在:

    • Thread -> ThreadLocalMap -> threadLocal -> value
    • 这里 threadLocal 是一种 WeekReference 弱引用,会被 GC。
    • 所以唯一可能滞留在内存中不会被 GC 的就是强引用的 value,那就有可能造成 OOM。
  • JDK 已经考虑了这个问题,所以在 set、remove、rehash 方法中会扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就可以被回收。

    if (k == null) {
        replaceStaleEntry(key, value, i);
        return;
    }
    
  • 但是如果一个 ThreadLocal 不被使用,那么实际上 set、remove、rehash 方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就有可能导致 value 的内存泄露。

避免:(阿里规约)

  • 在线程使用 ThreadLocal 的最后一个方法执行后调用 remove() 方法删除对应的 Entry 对象。

# 4.2 空指针异常

  • 没有 set() 也没有重写 initialValue() 的情况下直接 get(),如果存储在 ThreadLocal 中的对象是基本数据类型的封装类,在接受对象的时候使用基本数据类型来接收,比如用 long 来接收 Long,那么一个 null 的 Long 在自动拆箱的时候就会报 NullPointerException。

# 4.3 共享对象

  • 如果在每个线程中 ThreadLocal.set() 进去的东西本身就是多线程共享的同一个对象,比如 static 对象,那么多个线程的 ThreadLocal.get() 取得的对象还是这个共享对象本身,而不是每个线程独享的,那么还是有并发访问问题。

# 4.4 使用需要

  • 没必要不使用,麻烦。
  • 优先使用框架的支持,而不是自己创建。

# 5. ThreadLocal 在 Spring 中的应用

  • DateTimeContextHolder<DateTimeContext>:存时间
  • RequestContextHolder<RequestAttributes>:存请求参数
上次更新: 11/1/2021, 10:07:49 AM