关于springboot:12ThreadLocal的那点小秘密

37次阅读

共计 5223 个字符,预计需要花费 14 分钟才能阅读完成。

好久不见,不晓得大家新年过得怎么样?有没有痛痛快快得放松?是不是还能收到很多压岁钱?好了,话不多说,咱们开始明天的主题:ThreadLocal。
我收集了 4 个面试中呈现频率较高的对于 ThreadLocal 的问题:

什么是 ThreadLocal?什么场景下应用 ThreadLocal?
ThreadLocal 的底层是如何实现的?
ThreadLocal 在什么状况下会呈现内存透露?
应用 ThreadLocal 要留神哪些内容?

咱们先从一个“流言”开始,通过剖析 ThreadLocal 的源码,尝试纠正“流言”带来的误会,并解答下面的问题。
流传已久的“流言”
很多文章都在说“ThreadLocal 通过拷贝共享变量的形式解决并发平安问题”,例如:

这种说法并不精确,很容易让人误会为 ThreadLocal 会拷贝共享变量。来看个例子:
private static final DateFormat DATE_FORMAT = new SimpleDateFormat(“yyyy-MM-dd”);

public static void main(String[] args) throws InterruptedException {

for (int i = 0; i < 1000; i++) {new Thread(() -> {
        try {System.out.println(DATE_FORMAT.parse("2023-01-29"));
        } catch (ParseException e) {e.printStackTrace();
        }
    }).start();}

}
复制代码
咱们晓得,多线程并发拜访同一个 DateFormat 实例对象会产生重大的并发平安问题,那么退出 ThreadLocal 是不是能解决并发平安问题呢?批改下代码:
/**

  • 第一种写法
    */

private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() {

@Override
protected DateFormat initialValue() {return DATE_FORMAT;}

};

public static void main(String[] args) throws InterruptedException {

for (int i = 0; i < 1000; i++) {new Thread(() -> {
        try {System.out.println(DATE_FORMAT_THREAD_LOCAL.get().parse("2023-01-29"));
        } catch (ParseException e) {e.printStackTrace();
        }
    }).start();}

}
复制代码
预计会有很多小伙伴会说:“你这么写不对!《阿里巴巴 Java 开发手册》中不是这么用的!”。把书中的用法搬过去:
/**

  • 第二种写法
    */

private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() {

@Override
protected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");
}

};
复制代码
Tips:代码小改了一下~~
咱们来看两种写法的差异:

第一种写法,ThreadLocal#initialValue 时应用共享变量 DATE_FORMAT;
第二种写法,ThreadLocal#initialValue 时创立 SimpleDateFormat 对象。

依照“流言”的形容,第一种写法会拷贝 DATE_FORMAT 的正本提供给不同的线程应用,但从后果上来看 ThreadLocal 并没有这么做。
有的小伙伴可能会狐疑是因为 DATE_FORMAT_THREAD_LOCAL 线程共享导致的,但别忘了第二种写法也是线程共享的。
到这里咱们应该可能猜到,第二种写法中每个线程会拜访不同的 SimpleDateFormat 实例对象,接下来咱们通过源码一探到底。
ThreadLocal 的实现
除了应用 ThreadLocal#initialValue 外,还能够通过 ThreadLocal#set 增加变量后再应用:
ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
threadLocal.set(new SimpleDateFormat(“yyyy-MM-dd”));
System.out.println(threadLocal.get().parse(“2023-01-29”));
复制代码
Tips:这么写仅仅是为了展现用法~~
应用 ThreadLocal 非常简单,3 步就能够实现:

创建对象
增加变量
取出变量

无参结构器没什么好说的(空实现),咱们从 ThreadLocal#set 开始。
ThreadLocal#set 的实现
ThreadLocal#set 的源码:
public void set(T value) {,

Thread t = Thread.currentThread();

// 获取以后线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);

if (map != null) {
    // 增加变量
    map.set(this, value);
} else {
    // 初始化 ThreadLocalMap
    createMap(t, value);
}

}
复制代码
ThreadLocal#set 的源码非常简单,但却走漏出了不少重要的信息:

变量存储在 ThreadLocalMap 中,且与以后线程无关;
ThreadLocalMap 应该相似于 Map 的实现。

接着来看源码:
public class ThreadLocal<T> {

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

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

}

public class Thread implements Runnable {

ThreadLocal.ThreadLocalMap threadLocals = null;

}
复制代码
很清晰的展现出 ThreadLocalMap 与 Thread 的关系:ThreadLocalMap 是 Thread 的成员变量,每个 Thread 实例对象都领有本人的 ThreadLocalMap。
另外,还记得在对于线程你必须晓得的 8 个问题(上)提到 Thread 实例对象与执行线程的关系吗?

如果从 Java 的层面来看,能够认为创立 Thread 类的实例对象就实现了线程的创立,而调用 Thread.start0 能够认为是操作系统层面的线程创立和启动。

能够近似的看作是:Thread 实例对象≈执行线程 Thread 实例对象 \approx 执行线程 Thread 实例对象≈执行线程。也就是说,属于 Thread 实例对象的 ThreadLocalMap 也属于每个执行线程。
基于以上内容,咱们如同失去了一个非凡的变量作用域:属于线程。
Tips:

实际上属于线程也即是属于 Thread 实例对象,因为 Thread 是线程在 Java 中的形象;
ThreadLocalMap 属于线程,但不代表存储到 ThreadLocalMap 的变量属于线程。

ThreadLocalMap 的实现
ThreadLocalMap 是 ThreadLocal 的外部类,代码也不简单:
public class ThreadLocal<T> {

private final int threadLocalHashCode = nextHashCode();

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
    
        Object value;
        
        Entry(ThreadLocal<?> k, Object v) {super(k);
            value = v;
        }
    }
    
    private Entry[] table;
    
    private int size = 0;
    
    private int threshold;
    
    private void setThreshold(int len) {threshold = len * 2 / 3;}
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
}

}
复制代码
仅从构造和构造方法中曾经可能窥探到 ThreadLocalMap 的特点:

ThreadLocalMap 底层存储构造是 Entry 数组;
通过 ThreadLocal 的哈希值取模定位数组下标;
构造方法增加变量时,存储的是原始变量。

很显著,ThreadLocalMap 是哈希表的一种实现,ThreadLocal 作为 Key,咱们能够将 ThreadLocalMap 看做是“简版”的 HashMap。
Tips:

本文不探讨哈希表实现中解决哈希抵触,数组扩容等问题的形式;
也不须要关注 ThreadLocalMap#set 和 ThreadLocalMap#getgetEntry 的实现;
与构造方法一样,ThreadLocalMap#set 中存储的是原始变量。

到目前为止,无论是 ThreadLocalMap#set 还是 ThreadLocalMap 的构造方法,都是存储原始变量,没有任何拷贝正本的操作。也就是说,想要通过 ThreadLocal 实现变量在线程间的隔离,就须要手动为每个线程创立本人的变量。
ThreadLocal#get 的实现
ThreadLocal#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();

}
复制代码
后面的局部很容易了解,咱们看 map == null 时调用的 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);
}

if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;

}
复制代码
ThreadLocal#setInitialValue 办法简直和 ThreadLocal#set 一样,但变量是通过 ThreadLocal#initialValue 取得的。如果是通过 ThreadLocal#initialValue 增加变量,在第一次调用 ThreadLocal#get 时将变量存储到 ThreadLocalMap 中。
ThreadLocal 的原理
好了,到这里咱们曾经能够构建出对 ThreadLocal 比拟残缺的认知了。咱们先来看 ThreadLocal,ThreadLocalMap 和 Thread 三者之间的关系:

能够看到,ThreadLocal 是作为 ThreadLocalMap 中的 Key 的,而 ThreadLocalMap 又是 Thread 中的成员变量,属于每一个 Thread 实例对象。遗记 ThreadLocalMap 是 ThreadLocal 的外部类这层关系,整体构造就会十分清晰。
创立 ThreadLocal 对象并存储数据时,会为每个 Thread 对象创立 ThreadLocalMap 对象并存储数据,ThreadLocal 对象作为 Key。在每个 Thread 对象的生命周期内,都能够通过 ThreadLocal 对象拜访到存储的数据。

正文完
 0