前言

面试的时候被问到ThreadLocal的相关知识,没有回答好(奶奶的,现在感觉问啥都能被问倒),所以我决定先解决这几次面试中都遇到的高频问题,把这几个硬骨头都能理解的透彻的说出来了,感觉最起码不能总是一轮游。

ThreadLocal介绍

ThreadLocal是JDK1.2开始就提供的一个用来存储线程本地变量的类。ThreadLocal中的变量是在每个线程中独立存在的,当多个线程访问ThreadLocal中的变量的时候,其实都是访问的自己当前线程的内存中的变量,从而保证的变量的线程安全。

我们一般在使用ThreadLocal的时候都是为了解决线程中存在的变量竞争问题。其实解决这类问题,通常大家也会想到使用synchronized来加锁解决。

例如在解决SimpleDateFormat的线程安全的时候。SimpleDateFormat是非线程安全的,它里面无论的是format()方法还是parse()方法,都有使用它自己内部的一个Calendar类的对象,format方法是设置时间,parse()方法里面是先调用Calendar的clear()方法,然后又调用了Calendar的set()方法(赋值),如果一个线程刚调用了set()进行赋值,这个时候又来了一个线程直接调用了clear()方法,那么这个parse()方法执行的结果就会有问题的。

解决办法一

将使用SimpleDateformat的方法加上synchronized,这样虽然保证了线程安全,但却降低了效率,同一时间只有一个线程能使用格式化时间的方法。

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static synchronized String formatDate(Date date){
return simpleDateFormat.format(date);
}

解决办法二

将SimpleDateFormat的对象,放到ThreadLocal里面,这样每个线程中都有一个自己的格式对象的副本了。互不干扰,从而保证了线程安全。

private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static String formatDate(Date date){
return simpleDateFormatThreadLocal.get().format(date);
}

ThreadLocal的原理

我们先看一下ThreadLocal是怎么使用的。

ThreadLocal<Integer> threadLocal99 = new ThreadLocal<Integer>();
threadLocal99.set(3);
int num = threadLocal99.get();
System.out.println("数字:"+num);
threadLocal99.remove();
System.out.println("数字Empty:"+threadLocal99.get());

运行结果:

数字:3
数字Empty:null

使用起来很简单,主要是将变量放到ThreadLocal里面,在线程执行过程中就可以取到,当执行完成后在remove掉就可以了,只要没有调用remove()当前线程在执行过程中都是可以拿到变量数据的。

因为是放到了当前执行的线程中,所以ThreadLocal中的变量值只能当前线程来使用,从而保证的了线程安全(当前线程的子线程其实也是可以获取到的)。

来看一下ThreadLocal的set()方法源码

public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取ThreadLocalMap
ThreadLocal.ThreadLocalMap map = getMap(t);
// ThreadLocalMap 对象是否为空,不为空则直接将数据放入到ThreadLocalMap中
if (map != null)
map.set(this, value);
else
createMap(t, value); // ThreadLocalMap对象为空,则先创建对象,再赋值。
}

我们看到变量都是存放在了ThreadLocalMap这个变量中的。那么ThreadLocalMap又是怎么来的呢?

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public class Thread implements Runnable {
... ...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
... ...
}

通过上面的源码,我们发现ThreadLocalMap变量是当前执行线程中的一个变量,所以说,ThreadLocal中存放的数据其实都是放到了当前执行线程中的一个变量里面了。也就是存储在了当前的线程对象里了,别的线程里面是另一个线程对象了,拿不到其他线程对象中的数据,所以数据自然就隔离开了。

那么ThreadLocalMap是怎么存储数据的呢?

ThreadLocalMap 是ThreadLocal类里的一个内部类,虽然类的名字上带着Map但却没有实现Map接口,只是结构和Map类似而已。



ThreadLocalMap内部其实是一个Entry数组,Entry是ThreadLocalMap中的一个内部类,继承自WeakReference,并将ThreadLocal类型的对象设置为了Entry的Key,以及对Key设置成弱引用。

ThreadLocalMap的内部数据结构,就大概是这样的key,value组成的Entry的数组集合。



和真正的Map还是有区别的,没有链表了,这样在解决key的hash冲突的时候措施肯定就和HashMap不一样了。

一个线程中是可以创建多个ThreadLocal对象的,多个ThreadLocal对象就会存放多个数据,那么在ThreadLocalMap中就会以数组的形式存放这些数据。

我们来看一下具体的ThreadLocalMap的set()方法的源码

/**
* Set the value associated with key.
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not. 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)]) {
ThreadLocal<?> k = e.get();
// 如果当前位置不为空,并且当前位置的key和传过来的key相等,那么就会覆盖当前位置的数据
if (k == key) {
e.value = value;
return;
}
// 如果当前位置为空,则初始化一个Entry对象,放到当前位置。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果当前位置不为空,并且当前位置的key也不等于要赋值的key ,那么将去找下一个空位置,直接将数据放到下一个空位置处。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

我们从set()方法中可以看到,处理逻辑有四步。

  • 第一步先根据Threadlocal对象的hashcode和数组长度做与运算获取数据应该放在当前数组中的位置。

  • 第二步就是判断当前位置是否为空,为空的话就直接初始化一个Entry对象,放到当前位置。

  • 第三步如果当前位置不为空,而当前位置的Entry中的key和传过来的key一样,那么直接覆盖掉当前位置的数据。

  • 第四步如果当前位置不为空,并且当前位置的Entry中的key和传过来的key

    也不一样,那么就会去找下一个空位置,然后将数据存放到空位置(数组超过长度后,会执行扩容的);

在get的时候也是类似的逻辑,先通过传入的ThreadLocal的hashcode获取在Entry数组中的位置,然后拿当前位置的Entry的Key和传入的ThreadLocal对比,相等的话,直接把数据返回,如果不相等就去判断和数组中的下一个值的key是否相等。。。

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length; while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

我们上文一直说,ThreadLocal是保存在单个线程中的数据,每个线程都有自己的数据,但是实际ThreadLocal里面的真正的对象数据,其实是保存在堆里面的,而线程里面只是存储了对象的引用而已。

并且我们在使用的时候通常需要在上一个线程执行的方法的上下文共享ThreadLocal中的变量。

例如我的主线程是在某个方法中执行代码呢,但是这个方法中有一段代码时新创建了一个线程,在这个线程里面还使用了我这个正在执行的方法里面的定义的ThreadLocal里面的变量。这个时候,就是需要从新线程里面调用外面线程的数据,这个就需要线程间共享了。这种子父线程共享数据的情况,ThreadLocal也是支持的。

例如:

 ThreadLocal threadLocalMain = new InheritableThreadLocal();
threadLocalMain.set("主线程变量");
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println( "现在获取的变量是 =" + threadLocalMain.get());
}
};
t.start();

运行结果:

现在获取的变量是 =主线程变量

上面这样的代码就能实现子父线程共享数据的情况,重点是使用InheritableThreadLocal来实现的共享。

那么它是怎么实现数据共享的呢?

在Thread类的init()方法中有这么一段代码:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这段代码的意思是,在创建线程的时候,如果当前线程的inheritThreadLocals变量和父线程的inheritThreadLocals变量都不为空的时候,会将父线程的inheritThreadLocals变量中的数据,赋给当前线程中的inheritThreadLocals变量。

ThreadLocal的内存泄漏问题

上文我们也提到过,ThreadLocal中的ThreadLocalMap里面的Entry对象是继承自WeakReference类的,说明Entry的key是一个弱引用。

弱引用是用来描述那些非必须的对象,弱引用的对象,只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

这个弱引用还是ThreadLocal对象本身,所以一般在线程执行完成后,ThreadLocal对象就会变成null了,而为null的弱引用对象,在下一次GC的时候就会被清除掉,这样Entry的Key的内存空间就被释放出来了,但是Entry的value还在占用的内存,如果线程是被复用的(例如线程池中的线程),那么这里面的value值就会越来越多,最终就导致了内存泄漏。

防止内存泄漏的办法就是在每次使用完ThreadLocal的时候都去执行以下remove()方法,就可以把key和value的空间都释放了。

那既然容易产生内存泄漏,为什么还要设置成弱引用的呢?

如果正常情况下应该是强引用,但是强引用只要引用关系还在就一直不会被回收,所以如果线程被复用了,那么Entry中的Key和Value都不会被回收,这样就造成了Key和Value都会发生内存泄漏了。

来讲讲你对ThreadLocal的理解的更多相关文章

  1. ThreadLocal深入理解二

    转载:http://doc00.com/doc/101101jf6 今天在看之前转载的博客:ThreadLocal的内部实现原理.突然有个疑问, 按照threadLocal的原理, 当把一个对象存入到 ...

  2. ThreadLocal深入理解一

    转载:http://www.cnblogs.com/dolphin0520/p/3920407.html 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使 ...

  3. Java中的ThreadLocal深入理解

    提到ThreadLocal,有些Android或者Java程序员可能有所陌生,可能会提出种种问题,它是做什么的,是不是和线程有关,怎么使用呢?等等问题,本文将总结一下我对ThreadLocal的理解和 ...

  4. ThreadLocal的理解与应用场景分析

    对于Java ThreadLocal的理解与应用场景分析 一.对ThreadLocal理解 ThreadLocal提供一个方便的方式,可以根据不同的线程存放一些不同的特征属性,可以方便的在线程中进行存 ...

  5. Python中ThreadLocal的理解与使用

    一.对 ThreadLocal 的理解 ThreadLocal,有的人叫它线程本地变量,也有的人叫它线程本地存储,其实意思一样. ThreadLocal 在每一个变量中都会创建一个副本,每个线程都可以 ...

  6. java中threadlocal的理解

    [TOC] #java中threadlocal的理解##一.threadlocal的生命周期和ThreadLocalMap的生命周期可以吧TreadLocal看做是一个map来使用,只不过这个map是 ...

  7. ThreadLocal简单理解

    在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...

  8. threadlocal彻底理解,深刻

    本文转自http://blog.csdn.net/huachao1001/article/details/51970237 ThreadLocal的使用相信大家都比较熟悉,但是ThreadLocal内 ...

  9. 我对ThreadLocal的理解

    声明:小弟菜狗一个.对ThreadLocal的描写叙述和理解难免有所偏差 近期由于须要深入的了解android的handler消息机制而去查看了Looper的源代码.众所周知在主线程中是不须要在程序猿 ...

随机推荐

  1. 37 Reasons why your Neural Network is not working

    37 Reasons why your Neural Network is not working Neural Network Check List 如何使用这个指南 数据问题 检查输入数据 试一下 ...

  2. 深度学习 | 训练网络trick——mixup

    1.mixup原理介绍 mixup 论文地址 mixup是一种非常规的数据增强方法,一个和数据无关的简单数据增强原则,其以线性插值的方式来构建新的训练样本和标签.最终对标签的处理如下公式所示,这很简单 ...

  3. 2020-06-07:mysql中varchar类型的id,where id=1,会用到索引吗?int 类型的id,where id="1",会用到索引吗?为什么?

    福哥答案2020-06-07: 答案来自群员:对于int类型id,查询的varchar 类型 ‘1’会隐式转换成 1,‘1’和 1都能正常走索引:对于varchar类型id,查询的int 类型 1不会 ...

  4. C#算法设计排序篇之08-计数排序(附带动画演示程序)

    计数排序(Counting Sort) 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/689 访问. 计数排序是一个非基 ...

  5. C#LeetCode刷题之#561-数组拆分 I(Array Partition I)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3718 访问. 给定长度为 2n 的数组, 你的任务是将这些数分成 ...

  6. Linux内核之 进程调度

    上一篇我们提到过进程状态,而进程调度主要是针对TASK_RUNNING运行状态进行调度,因为其他状态是不可执行比如睡眠,不需要调度. 1.进程调度概念 进程调度程序,简称调度程序,它是确保进程能有效工 ...

  7. oracle语法查某个字段为空

    select * from t_address_express_mapping t where t.start_house_number is null;

  8. 求正整数2和n之间的完全数

    [题目描述] 求正整数22和nn之间的完全数(一行一个数). 完全数:因子之和等于它本身的自然数,如6=1+2+36=1+2+3 [输入] 输入n(n≤5000)n(n≤5000). [输出] 一行一 ...

  9. ARM伪指令与伪操作

    一.伪指令 ARM伪指令有四个,分别是LDR.ADR.ADRL和NOP,下边对其分别介绍. 1.1 LDR LDR 伪指令用于加载 32 位的立即数或一个地址值到指定寄存器 .形式如  LDR{con ...

  10. 创建SpringMVC项目

    学习SpringMVC框架第一步,先创建一个简单项目,了解流程.使用的是Eclipse+Tomcat9.0 创建项目springmvc 新建Dynamic Web Project File->N ...