ThreadLocal at a galance

126 阅读6分钟

ThreadLocal探析

ThreadLocal是常出现的Java基础面试题,掌握ThreadLocal不仅仅是为了面试,还可以为线程安全提供多一种思路。

是什么

一句话来说,ThreadLocal是用于实现线程本地变量的类,这里的线程本地变量不是指对象分配在栈上,而是指不同的线程中包含的是各自的实例,而非从进程处继承来的变量值。

关键设计

在讲解如何实现线程本地变量之前,我们需要明确一些关键设计。

Thread中的threadLocals成员

Thread类是JDK提供的线程类,用于ThreadPoolExecutor底层实现。在Thread中有如下代码:

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

可以看得出这里每个Thread对象都将拥有自己的threadLocals成员变量,这个是实现线程本地变量的基础,说白了就是线程自己的变量存在自己的threadLocals中。

ThreadLocalMap类

再来近看一眼ThreadLocalMap这个类,从上面代码其实已经可以看出,它是ThreadLocal的一个内部类:

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;
            }
        }
    
		/**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
    
    //省略其他代码
    ......
}

这里可以看出ThreadLocal的基本结构,一个Entry类型的数组用于存储具体数据。Entry本身是一种弱引用,它引用的对象为ThreadLocal变量,并拓展了value成员变量用于存放所谓的线程本地对象。

如何协作

有了上面的知识,我们离真相也就进了一步, 但是Thread的threadLocals成员变量究竟是如何去存取值的呢?这里就要请出本厂的主角ThreadLocal类本尊了。

ThreadLocal的set方法

剪短来说,其实只要关注ThreadLocal的get和set方法就能找到入口了:

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

public void set(T value) {
    /*
    这里是关键,ThreadLocal的set方法其实就是根据这里实现了线程本地特性的,
    getMap方法会返回当前线程的ThreadLocalMap成员,即threadLocals用于存放
    本地变量
    */
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    
    //存入变量。map不存在则创建,这里值得注意的是,map的key不会为null
    //key的本身是当前ThreadLocal对象
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap的set方法

再进一步看一下ThreadLocalMap的set方法:

private void set(ThreadLocal<?> key, Object value) {

    //ThreadLocalMap本质是Entry数组
    Entry[] tab = table;
    
    //计算下标位置用于存放Entry,这里注意下标i与Entry数组长度有关
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    //如果下标位置为非空Entry对象,则进入循环
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        //获取Entry包含的弱引用
        ThreadLocal<?> k = e.get();

        //弱引用存在,且变量与要设置的值一致,直接返回
        if (k == key) {
            e.value = value;
            return;
        }

        //弱引用不存在,说明旧值已被回收,换为新值
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //如果为经过循环,说明本位置Entry值为null,直接设置为要存的值
    tab[i] = new Entry(key, value);
    int sz = ++size;
    
    //之前提到下标和数组长度有关,这是此处rehash的原因
    //rehash的部分不再此处说明
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocal的get方法

public T get() {
    //熟悉的操作
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    
    if (map != null) {
        //之前set方法中说过,key是当前ThreadLocal本身,所以用this做key获取到值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    //map未初始化,则初始化之,将值设置为初始默认值,并返回该值
    return 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;
}

//这个方法时protected的,使用ThreadLocal时需要覆写
protected T initialValue() {
    return null;
}

实例演示

有了之前的一些知识,我们可以来上代码了。还是从逃不过的DateFormat开始

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateTimeUtil {

   //Java 8特性加持写法
   private  static ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(
         ()->{
            System.out.println(Thread.currentThread().getName() + " trigger initialValue");
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         }
   );
    
    //第二种写法
    private  static ThreadLocal<SimpleDateFormat> sdf2= new ThreadLocal<SimpleDateFormat>(){
			@Override
			protected SimpleDateFormat initialValue(){
				System.out.println(Thread.currentThread().getName() + " trigger initialValue");
				return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			}
	};

   public static Date parse(String dateStr) throws ParseException {
       //打印当前线程以及线程中的值可知每个线程不一样
       System.out.println(Thread.currentThread().getName() + " is calling parse!" + dateStr + " ref:" + System.identityHashCode(sdf.get()));
      return sdf.get().parse(dateStr);
   }
}

如果使用两个线程来调用parse方法可以明显看到结果不同,大致如下

thread 1 trigger initialValue
thread 1 is calling parse!1990-1-1 00:00:00 ref:218991134
thread 2 trigger initialValue
thread 2 is calling parse!2019-8-1 16:04:02 ref:131510706

可以从ref看出两者的SimpleDateFormat不是同一个。

如果用线程池,让一个线程多调用几次又会如何呢?我创建了一个核心线程和最大线程数均为1的线程池,反复调用,结果如下:

pool-1-thread-1 trigger initialValue
pool-1-thread-1 is calling parse!2019-8-1 16:04:02 ref:848068023
pool-1-thread-1 is calling parse!2019-8-1 16:07:31 ref:848068023
...
pool-1-thread-1 is calling parse!2019-8-1 16:07:31 ref:848068023

可知该ThreadLocal不断在复用,这有什么好处呢?

这里之所以选用SimpleDateFormat来讲,其实是因为他是线程不安全的,笔者之前就掉过坑,具体的原因很多文章有说,这里就不再详述,既然线程不安全,那么就不能再多线程中使用,那该咋办,最简单的方法可以想得到:

public static Date parse(String dateStr) throws ParseException {
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr);
}

上面是一种方法,每次new一个。但是在方法被高频调用的情况下,对象的创建和销毁成本就会增加,对内存你的消耗也会上升。这个时候再回想ThreadLocal,我们会发现,ThreadLocal既解决了线程安全问题,也能保证对象创建和销毁成本较低,一举两得,毕竟仅在初次get的时候回调用setInitialValue方法初始化对象。现在再回头看,是否能理解我再最初说的为ThreadLocal为线程安全问题提供了多一种思路呢?毕竟对象时自己的,不是混用了,自然就没有数据竞争问题。