1. ThreadLocal 简介

synchronzed 或者 lock 是通过控制线程对临界区资源的同步顺序从而解决线程安全的问题。但是这种加锁会让未获取到锁的线程进行阻塞等待,时间效率不好。

线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,那么,如果每个线程都使用自己的「共享资源」,各自使用各自的,又互相不影响,即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。这也是一种空间换时间的方案。

  • 缺点是:内存会大很多;
  • 优点是:不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高的时间效率。


虽然 ThreadLocal 并不在 java.util.concurrent包中而在java.lang包中,但我更倾向于把它当作是一种并发容器(虽然真正存放数据的是 ThreadLoclMap)进行归类。从ThreadLocal 这个类名可以顾名思义的进行理解,表示线程的「本地变量」,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的这样就可以避免共享资源的竞争

  • 保存线程上下文信息,在任意需要的地方可以获取!!!
  • 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!

2. ThreadLocal 的实现原理

2.1 set 方法

**set** 方法设置在当前线程中 threadLocal 变量的值,该方法的源码为:

  1. public void set(T value) {
  2. //1. 获取当前线程实例对象
  3. Thread t = Thread.currentThread();
  4. //2. 通过当前线程实例获取到 ThreadLocalMap 对象
  5. ThreadLocalMap map = getMap(t);
  6. if (map != null)
  7. //3. 如果Map不为null,则以当前 threadLocl 实例为 key,值为 value 进行存入
  8. map.set(this, value);
  9. else
  10. //4.map为null,则新建ThreadLocalMap并存入value
  11. createMap(t, value);
  12. }

数据 value 是真正的存放在了 ThreadLocalMap 这个容器中了,并且是以当前 threadLocal 实例为 key

首先 ThreadLocalMap 是怎样来的?源码很清楚,是通过getMap(t)进行获取:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

该方法直接返回的就是当前线程对象 t 的一个成员变量 threadLocals:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

也就是说 ThreadLocalMap 的引用是作为 Thread 的一个成员变量,被 Thread 进行维护的。回过头再来看看set方法,当 map 为 Null 的时候会通过createMap(t,value)方法:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

该方法就是 new 一个 ThreadLocalMap 实例对象,然后同样以当前 threadLocal 实例作为 key,值为 value 存放到 threadLocalMap 中,然后将当前线程对象的 threadLocals 赋值为 threadLocalMap

现在来对 set 方法进行总结一下:

  • 通过当前线程对象 thread 获取该 thread 所维护的 threadLocalMap
  • 若 threadLocalMap 不为 null,则以 threadLocal 实例为 key,值为 value 的键值对存入 threadLocalMap
  • 若 threadLocalMap 为 null 的话,就新建 threadLocalMap 然后在以 threadLocal 为键,值为 value 的键值对存入即可。

2.2 get 方法

get 方法是获取当前线程中 threadLocal 变量的值,同样的还是来看看源码:

public T get() {
    //1. 获取当前线程的实例对象
    Thread t = Thread.currentThread();
    //2. 获取当前线程的threadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //3. 获取map中当前threadLocal实例为key的值的entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //4. 当前entitiy不为null的话,就返回相应的值value
            T result = (T)e.value;
            return result;
        }
    }
    //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
    return setInitialValue();
}

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

这段方法的逻辑和set方法几乎一致,另外值得关注的是initialValue方法:

protected T initialValue() {
    return null;
}

这个方法是**protected**修饰的也就是说继承**ThreadLocal**的子类可重写该方法,实现赋值为其他的初始值。关于get方法来总结一下:

  • 通过当前线程 **thread** 实例获取到它所维护的 **threadLocalMap**
  • 然后以当前 **threadLocal** 实例为 **key** 获取该 **map** 中的键值对(Entry),若 **Entry** 不为 null 则返回 **Entry****value**。如果获取 **threadLocalMap** 为 null 或者 Entry 为 null 的话,就以当前 **threadLocal** 为 Key,**value** 为 null 存入 **map** 后,并返回 null。

2.3 remove 方法

public void remove() {
    //1. 获取当前线程的threadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
        //2. 从map中删除以当前threadLocal实例为key的键值对
        m.remove(this);
}

get,set方法实现了存数据和读数据,我们当然还得学会如何删数据。删除数据当然是从 map 中删除数据,先获取与当前线程相关联的 threadLocalMap 然后从 map 中删除该 threadLocal 实例为 key 的键值对即可。

3. ThreadLocalMap 详解

从上面的分析我们已经知道,数据其实都放在了threadLocalMap中,threadLocal 的 getsetremove方法实际上具体是通过 threadLocalMap 的getEntry,setremove方法实现的。

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;

通过注释可以看出,table数组的长度为2的幂次方。接下来看下 Entry 是什么:

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

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

Entry 是一个以ThreadLocal为 key,Object为 value 的键值对,另外需要注意的是这里的 threadLocal 是弱引用,因为 Entry 继承了 WeakReference,在 Entry 的构造方法中,调用了**super(k)**方法就会将 threadLocal 实例包装成一个 WeakReferenece。到这里我们可以用一个图来理解下thread,threadLocal,threadLocalMap,Entry之间的关系:

image.png
image.png
Thread 类有属性变量 threadLocals (类型是 ThreadLocal.ThreadLocalMap),也就是说每个线程有一个自己的 ThreadLocalMap ,所以每个线程往这个 ThreadLocal 中读写隔离的,并且是互相不会影响的。一个ThreadLocal 只能存储一个 Object 对象,如果需要存储多个 Object 对象那么就需要多个 ThreadLocal.

注意上图中的实线表示强引用,虚线表示弱引用。

  • 如图所示,每个线程实例中可以通过 threadLocals 获取到 threadLocalMap,而 threadLocalMap 实际上就是一个以 threadLocal 实例为 key,任意对象为 value 的 Entry 数组。
  • 当我们为 threadLocal 变量赋值,实际上就是以当前 threadLocal 实例为 key,值为 value 的 Entry 往这个threadLocalMap 中存放。
  • 需要注意的是 Entry 中的 key 是弱引用,当 threadLocal 外部强引用被置为 null (**threadLocalInstance=null**),那么系统 GC 的时候,根据可达性分析,这个 threadLocal 实例就没有任何一条链路能够引用到它,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap中就会出现key为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:**Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value** 永远无法回收,造成内存泄漏。
  • 当然,如果当前 thread 运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的,所以,threadLocal 的内存泄漏问题,是应该值得我们思考和注意的问题,关于这个问题可以看这篇文章——详解threadLocal内存泄漏问题

3.1 Entry数据结构

3.2 set方法

3.3 getEntry方法

3.4 remove

4. ThreadLocal的使用场景

ThreadLocal 不是用来解决共享对象的多线程访问问题的,数据实质上是放在每个 thread 实例引用的threadLocalMap,也就是说每个不同的线程都拥有专属于自己的数据容器(threadLocalMap),彼此不影响。因此 threadLocal 只适用于 共享对象会造成线程安全 的业务场景。比如 hibernate 中通过 threadLocal 管理 Session 就是一个典型的案例,不同的请求线程(用户)拥有自己的 session,若将 session 共享出去被多线程访问,必然会带来线程安全问题。下面,我们自己来写一个例子,SimpleDateFormat.parse 方法会有线程安全的问题,我们可以尝试使用 threadLocal 包装 SimpleDateFormat,将该实例不被多线程共享即可。

public class ThreadLocalDemo {
    private static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executorService.submit(new DateUtil("2019-11-25 09:00:" + i % 60));
        }
    }

    static class DateUtil implements Runnable {
        private String date;

        public DateUtil(String date) {
            this.date = date;
        }

        @Override
        public void run() {
            if (sdf.get() == null) {
                sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            } else {
                try {
                    Date date = sdf.get().parse(this.date);
                    System.out.println(date);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 如果当前线程不持有 SimpleDateformat 对象实例,那么就新建一个并把它设置到当前线程中,如果已经持有,就直接使用。另外,**if (sdf.get() == null){....}else{.....}**可以看出为每一个线程分配一个 SimpleDateformat 对象实例是从应用层面(业务代码逻辑)去保证的。
  • 在上面我们说过 threadLocal 有可能存在内存泄漏,在使用完之后,最好使用 remove 方法将这个变量移除,就像在使用数据库连接一样,及时关闭连接。

ThreadLocal无法解决共享对象的更新问题!

其他文章

深入分析 ThreadLocal 内存泄露问题