面试官:“先问一个问题,如何在多线程的环境下保证数据不被其他线程修改?”
可以把这个数据用 ThreadLocal
封装一下
面试官:“噢,那你说一下 ThreadLocal 是什么吧,它和用锁有什么不一样?”
ThreadLocal
提供了线程隔离的变量,它一般以 private static
的形式出现,可以视作类中的全局变量,只不过每个线程拥有自己的变量数据。
至于它和锁有什么不一样,我觉得这个问题就不太好,因为它们根本就不是一类东西。对于数据使用方面来说,锁主要解决的是并发环境下数据竞争的问题,而 ThreadLocal
根本就不是解决并发问题的,因为本身就不存在并发的业务。简单来说,ThreadLocal
就好比身份证,我们可以拿身份证去网吧,去坐高铁,去住酒店,但我们每个人都是用的自己的身份证,不存在什么公共身份证大家抢着用。
面试官:“噢,看来你对使用场景很明白嘛,那你讲讲要怎么保证线程隔离的呢?”
这个简单啊,谁的数据就直接给它自己保管就好了嘛,对应的在 Java 中就是把线程隔离的数据存在这个线程对应的 Thread
实例中。
面试官:“具体一点?”
JDK 的做法是其实是每一个 Thread
类中都保存了一个 ThreadLocalMap
,以 ThreadLocal
为 key,保存的数据为 value, 这样每个线程就能有多个线程隔离的数据啦。以下面这段代码为例
public class Main {
private static ThreadLocal<String> id = new ThreadLocal<>();
private static ThreadLocal<String> phone = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(()-> {
id.set("2");
phone.set("654321");
});
t1.start();
id.set("1");
phone.set("123456");
}
}
对应的结构图如下所示:
因此,对 ThreadLocal
的读写,其实就是对 Thread
中的 ThreadLocalMap
的读写
面试官:“可以,你刚才说到 Map 的读写了吧,那 ThreadLocalMap 和 HashMap 有什么差别呢?”
需要注意的是 ThreadLocalMap
虽然也叫 Map,但并没有继承集合类中的 Map
接口,最大的差别再处理冲突上是不同的,集合类中的 Map 使用链地址法来处理冲突,而 ThreadLocalMap
使用的是开放寻址法,简单的说就是如果冲突了,就找下一个索引的 Entry
,直到找到空的为止,对应的代码为:(PS:如果学过数据结构的话应该知道散列表的几种冲突处理,就不用看下面代码啦)
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 使用hash来确定索引
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
// 如果 tab[i] 已经被占了,而且不是更新操作(即 k != key),那么将索引+1,看看是否被占了
// e = tab[i = nextIndex(i, len)]
// private static int nextIndex(int i, int len) {
// return ((i + 1 < len) ? i + 1 : 0);
// }
}
...
}
面试官:"看来你还看过一些源码嘛,那 ThreadLocalMap 中 Entry 的 key 对 ThreadLocal 是弱引用你总应该知道的吧?"
当然,因为 ThreadLocal
实例一般有两个引用地方,一个是声明 ThreadLocal
的地方,还一个是 ThreadLocalMap
中 key 的引用,这就导致了一个问题,如果除了 ThreadLocalMap
外不再有引用的话,那么 ThreadLocal
实例还是无法释放,但是这个实例也再也不会被访问到了,也就是内存泄露。因此将其设为弱引用的话,如果没有其他外部强引用后也能被回收。
面试官:“虽然 ThreadLocal 被回收了,但是 Entry 还在啊,里面的 value 引用也是个内存泄露啊!”
自然是有相应对策的,在调用 ThreadLocal
的 get()
、set()
等方法时可能会清除ThreadLocalMap
中 key 为 null
的 Entry
对象。不过还是建议在编程时使用 remove()
把不用了数据删掉吧。另外就像开头里说的,建议是搭配 private static
使用的,毕竟卸载类还是比较少见的吧,既然和线程同寿命,也就不存在泄露不泄露的问题了哈~
面试官:“嗯,那 ThreadLocal 就聊到这里吧~”