ThreadLocal 的笔记
一、ThreadLocal的一些问题
1、和Synchronized 的区别
2、存储在JVM中的哪个区域
3、真的只是当前线程可见吗
4、会导致内存泄漏么
5、为什么用Entry数组而不是Entry对象
6、学习的框架中哪些用到了ThreadLocal
7、ThreadLocal里的对象一定是线程安全吗
问题答疑在最后。
二、ThreadLocal的概述
THreadLocal类是用来提供线程内部的局部变量。
让这些局部变量在多线程环境下访问(get/set)时能保证各个线程里的变量相对独立于其他线程内的变量。
通常的情况下,我们创建的成员变量都是线程不安全的,因为他可能被多个线程同时修改,
此变量对于多个线程之间彼此并不独立,是共享变量。而使用ThreadLocal创建的变量只能被当前线程访问,
其他线程无法访问与修改。也就是说,讲线程共有化变为线程私有化。
package thread.threadlocal;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author 超
* Create by fengc on 2020/8/22 12:35
* 应用场景:
* 每个线程都需要一个独立的对象
* (比如工具类,典型的就是SimpleDateFormat,
* 每次使用都需要new一次,很浪费性能,直接放到成员变量里又是线性不安全,所以直接放到ThreadLocal 管理)
*/
public class ThreadLocalTimeMain {
private static final ExecutorService executorService = Executors.newFixedThreadPool(2);
private final static AtomicInteger countAtomic = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int finalI = i;
executorService.execute(() -> {
String dateStr = ThreadLocalDateUtils.dateToStr(new Date(System.currentTimeMillis() + finalI*100));
//查看输出线程,
System.out.println("计数器" +countAtomic.incrementAndGet()+",-> " + Thread.currentThread().getName()+":" + dateStr);
});
}
executorService.shutdown();
}
}
package thread.threadlocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author 超
* Create by fengc on 2020/8/22 13:43
* 日期转化工具类
*/
public class ThreadLocalDateUtils {
/**
* Java8 直接初始化,lambam写法
*/
private final static ThreadLocal<SimpleDateFormat> ymdSimpleFormatThrread = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
private final static ThreadLocal<SimpleDateFormat> simpleFormatThread = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(Thread.currentThread().getName() + "<-> " + simpleDateFormat);
return simpleDateFormat;
}
};
/*
错误
static {
simpleFormatThread = new ThreadLocal<>();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("static<-->" + simpleDateFormat);
simpleFormatThread.set(simpleDateFormat);
}*/
/**
* 日期转为字符串
* @param date
* @return
*/
public static String dateToStr(Date date) {
SimpleDateFormat simpleDateFormat = simpleFormatThread.get();
System.out.println("-->" + simpleFormatThread);
return simpleDateFormat.format(date);
}
/**
* 字符串转为日期
* @param dateStr
* @return
* @throws ParseException
*/
public static Date strToDate(String dateStr) throws ParseException {
SimpleDateFormat simpleDateFormat = simpleFormatThread.get();
Date date = null;
try {
date = simpleDateFormat.parse(dateStr);
} catch (ParseException pe) {
pe.printStackTrace();
}
return date;
}
}
上面的代码运行则可发现,就是每个线程去创建一个SimpleDateFormat,当一个请求进来,就是一个线程,假如此线程会贯穿多个转换时间的方法dateToStr(),假如不用ThreadLocal的话,那么则要new三次,产生三个对象,那么使用了ThreadLocal,一个线程仅仅会产生一个对象。
应用场景:
- 每个线程都需要一个独享的对象,比如工具类,日期转换对象,SimpleDateFormat;
- 每个线程内需要保存全局变量(比如在登录成功后将用户信息保存到ThreadLocal里,然后当前线程操作的业务逻辑直接get取就完事了,有效地避免了参数的来回传递的麻烦之处);
- 比如存储交易id等信息,每个线程私有;
- 比如aop里记录日志需要before记录请求id,end拿出请求id(统计一个方法请求总时间);
- 比如jdbc连接池。
三、核心知识
-
类关系
每个Thread对象都持有一个ThreadLocalMap的成员变量,每个ThreadLocalMap内部又维护了N个Entry节点,也就是Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是代表THreadLocal的泛型值。 -
主要类与方法
public class Thread implements Runnable {
//每个线程Thread 都持有一个ThreadLocalMap对象
ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
//此类的实现是由子类继承实现,否则返回为空对象
protected T initialValue() {
return null;
}
//得到这个线程设置ThreadLocal对应的value,如果调用get之前没有进行set,则会内部执行initialValue()进行初始化。
public T get() {
//获取当前的线程对象
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象。
ThreadLocalMap map = getMap(t);
//当当前线程的ThreadLocalMap不为空的时候,那么则进入
if (map != null) {
//获取当前线程ThreadLocalMap的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
//当拿到了entry,则可以拿到了value了
if (e != null) {
@SuppressWarnings("unchecked")
// 直接获取entry对应的value返回。
T result = (T)e.value;
return result;
}
}
//初始化的方法,是延迟加载,只有当get的时候才会用到,当能获取到ThreadLocal的value值的时候,那么则不就执行到。
//一般来说,此初始化方法也只是会执行一次,但是当执行过remove方法后,再执行get的话,也会执行
return setInitialValue();
}
/**
* 设置当前线程的线程局部变量的值
* 此方法所做的事情跟initialValue()所做的事情是一样的,都是set值,只是当调用的时机不同。
* 实际上ThreadLocal的值是放入到当前线程的一个ThreadLocalMap的实例Entry[]的tables的,所以只能本地线程访问。
* @param value
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap实例,把当前线程的对象作为传输值,那么获取到的ThreadLcoalMap就是当前线程的。
ThreadLocalMap map = getMap(t);
//若是当前线程对应的ThreadLocalMap实例不为空,那么则把当前的ThreadLocal作为key,value作为值,设置到当前线程的ThreadLocalMap的数组Entrey中
if (map != null)
map.set(this, value);
else
//若是当前线程没有对应的ThreadLocalMap,则创建ThreadLocalMap,并绑定到当前线程中
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//直接删除当前ThreadLocal为key的value值
m.remove(this);
}
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
//ThreadLocalMap内部有Entry静态内部类,Entry的内部key是ThreadLocal本身,value是泛型
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
- initialValue() : 初始化。在get方法里面懒加载的。
- get() : 得到这个线程设置ThreadLocal对应的value,如果调用get之前没有进行set,则会内部执行initialValue()进行初始化。
- set() : 为这个线程设置一个value的新值
- remove() : 删除这个线程对应的value值,防止内存泄漏的最佳手段
3.线程中的ThreadLocalMap
此类虽然是ThreadLocal里面的静态,但是这个类是在Thread类里面创建的,归线程Thread所有。ThreadLocalMap类的内部是一个Entry数组,并不是一个Entry对象。因为在平时业务代码使用过程中,会创建很对各ThreadLocald对象,各司其职。但是在一次请求,就是一个线程,所以一个线程也就只有一个ThreadLocalMap,所以不管ThreadLocald创建多少个,ThreadLocalMap在一个线程里面也就只有一个,所以ThreadLocalMap里面是放一个Entry[]数组,存放的是一个线程里new出来的多个ThreadLocal对象。
四、重要的小结
ThrealLocal理解,主要是把几个类的关系了解清楚。
Thread、ThreadLocal、ThreadLocalMap、Entry
总结就是:线程Thread 维护了ThreadLocalMap,而ThreadLocalMap里面维护了数组Entry,在数组Entry里面存的是以ThreadLocal作为key,传入的值作为value的键值对。
五、面试题答疑(自己的白话答疑)
1. 和Synchronized的区别。(他是否和线程同步机制Synchronized提供同样的功能。)
其实不是。同步机制(如Synchronized)保证的是多线程同时操作共享变量并且能正确
输出结果的。ThreadLocal则是不行的,他仅仅是可以把共享变量变为线程私有,每个线程都有
独立的一个变量。
比如:网站计数器,每个请求计算一次,那么变量count++,带上Synchronized即可解决,
使用THreadLocal 是无法统计的,他只能用来保存一个线程使用到的用户登录信息,
方法跟着id等。
2、ThreadLcoal存储在jvm的哪个区域。(线程私有,是不是ThreadLocal的实例和他的值
就放在栈上)
不是。ThreadLocal的对象也是对象,还是放在堆上的。JVM通过一些技巧将其可见性变为线程可见。
3、ThreadLocal真的只是当前线程可见吗
不是单单是当前线程可见的,可以其他线程操作访问同一个ThreadLocal对象,但是这样操作,是不安全的。
4、ThreadLocal会导致内存泄漏么?
ThreadLocal的内存泄漏主要是说线程Thread存在ThreadLocalMap.Entry所存的key与values是否会内存泄漏。
那么可以分析的是:
- ThreadLocalMap.Entry 的key会内存泄漏吗?
- ThreadLocalMap.Entry 的value会内存泄漏吗?
看源代码:
// Entry,里面保存在ThreadLocal变量,也就是key,是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到源代码,发现是继承了弱引用,而且key是直接交给父类处理的,父类是一个弱引用,所以key是完全不存在内存泄漏问题,因为他不是强引用,他完全可以被GC回收的。
弱引用的特点:
如果这个对象只被弱引用关联,没有任何的强引用关联,那么这个对象就可以被GC回收掉,弱引用是不会阻止GC回收的。
可以再看value值,value值是一个强引用,但是不管强引用或者弱引用,线程终止了,那么都可以被GC回收掉的,引用引用链断了。(JVM的可达性分析法,
线程终止了,根结点Root就断了,下面的对象都会被回收)。
那么如此分析,ThreadLocal的使用并不会内存泄漏,但是一般使用线程的时候,都是会使用线程池的。线程池的存在核心线程是不会销毁的,只要创建出来就会反复地被利用,生命周期不会结束掉。
但是key是弱引用,会被GC回收掉,value是强引用,根结点还在,不会被回收掉,就会形成下面情形:
Thread -> ThreadLocalMap -> Entry(key=null)->value
由于value和Thread还存在着链路关系,还是可达的,所以不会被回收,假如线程中反复被创建如此多的垃圾对象无法被回收,那么内存就会存在泄漏,时间久了,就会OOM。
解决方案:
ThreadLocal已经为我们提供了remove的方法,所以在每个线程用完之后,最后记得调remove方法,把value移除。
5、ThreadLocalMap里面为什么是Entry数组,而不是Entry对象。
此问题主要是考ThreadLocalMap是在Thread里面持有的。
因为ThreadLocalMap是线程里面持有的,ThreadLocalMap的引用是在线程Thread里面的,所以在同一个线程中,也仅仅有只有一个ThreadLocalMap,而一个线程中,ThreadLocal的对象是会创建多个的,每个都有不同的用法。因为ThreadLocalMap中的Entry管理着以ThreadLocal为key,以泛型传进来值作为value的数据,所以ThreadLocalMap里面的Entry只能设置为数据吗,存储着所创建的多个ThreadLocal。
6、学习的开源框架中,哪里用到了ThreadLocal了
Spring框架
DateTimeContextHolder
RequestContextHolder
7、ThreadLocal里面的对象一定是线程安全的吗
不是。如果每个线程中的ThreadLocal.set()进去的对象,就是多线程共享的同一个对象,比如static,那么多线程的ThreadLcoal.get()获取的还是这个共享对象的本身,还是有并发访问线性不安全的问题。
六 ThreadLocal的一个简单的工具类
package thread.threadlocal;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author 超
* Create by fengc on 2020/8/23 00:00
* 以自己喜欢的key存入某个自己想要的对象到ThreadLocal中去
*/
public class ThreadLocalMapUtil<T> {
//创建一个ThreadLocalMap
private static final ThreadLocal<Map<String,Object>> threadLcoal = ThreadLocal.withInitial(HashMap::new);
//直接拿出Map
public static Map<String,Object> getThreadLocalMap() {
return threadLcoal.get();
}
/**
* 通过key拿出对象
* @param key
* @param <T>
* @return
*/
public static <T> T get(String key) {
Map<String,Object> map = threadLcoal.get();
@SuppressWarnings("unchecked")
T t = (T)map.get(key);
return t;
}
/**
* 获取不到返回默认值
* @param key
* @param defaulValue
* @param <T>
* @return
*/
public static <T> T get(String key,T defaulValue) {
Map<String,Object> map = threadLcoal.get();
@SuppressWarnings("unchecked")
T t = Objects.nonNull(map) ? (T)map.get(key) : defaulValue;
return t;
}
/**
* 设置值
* @param key
* @param value
*/
public static void set(String key,Object value) {
Map<String,Object> map = threadLcoal.get();
map.put(key,value);
}
/**
* 直接设置一个Map
* @param map
*/
public static void set(Map<String,Object> map) {
threadLcoal.get().putAll(map);
}
/**
* 移除当前的
*/
public static void remove() {
threadLcoal.remove();
}
}