1、ThreadLocal的使用场景
典型场景1
每个线程需要独享的对象(通常是工具类,如SimpleDateFormat和Random)
每个线程内拥有自己的实例副本,不共享
package com.imooc.thread_demo.threadlocal;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
/**
* @Author: zhangjx
* @Date: 2020/10/12 20:07
* @Description:
*/
public class SimpleDateFormatThreadLocalUtil {
@AllArgsConstructor
@Getter
enum SdfEnums{
UTC_FORMAT("yyyy-MM-dd'T'HH:mm:ss'Z'"),
CST_FORMAT("yyyy-MM-dd HH:mm:ss"),
DATE_FORMAT("yyyy-MM-dd");
private String sdf;
}
/**
* 存放不同的日期模板格式的sdf的Map
*/
private volatile static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<>();
private static SimpleDateFormat getSdf(final String pattern) {
ThreadLocal<SimpleDateFormat> sdf = sdfMap.get(pattern);
if (sdf == null) {
synchronized (SimpleDateFormatThreadLocalUtil.class) {
sdf = sdfMap.get(pattern);
if (sdf == null) {
sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat(pattern));
sdfMap.put(pattern, sdf);
}
}
}
return sdf.get();
}
public static String format(Date date, SdfEnums sdfEnums) {
return getSdf(sdfEnums.getSdf()).format(date);
}
public static Date parse(String dateStr, SdfEnums sdfEnums){
try {
return getSdf(sdfEnums.getSdf()).parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
IntStream.range(0, 1000).forEach(e -> {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " :" + format(new Date(), SdfEnums.CST_FORMAT));
}
});
});
}
}
典型场景2
每个线程需要保存全局变量(例如在拦截器中获取用户信息),避免参数传递
public class UserContextHolder {
public static final ThreadLocal<Long> USER_HOLDER = new InheritableThreadLocal<>();
public static void setUserId(Long userId){
USER_HOLDER.set(userId);
}
public static Long getUserId(){
return USER_HOLDER.get();
}
public static void remove(){
USER_HOLDER.remove();
}
}
2、使用ThreadLocal的好处
达到线程安全
不需要加锁,提高执行效率
更高效的利用内存,节省开销
避免繁琐的传参
3、ThreadLocal原理
ThreadLocal提供了一种方式,可以让线程在操作共享变量时,复制该共享变量的一个副本到线程自己的栈空间,以后就操作这个副本空间来代替共享空间。
Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对,key 就是 ThreadLocal 的引用,value是我们希望 ThreadLocal 存储的内容,例如 user 对象等。
ThreadLocal源码
public T get() {
Thread t = Thread.currentThread();
//获取到当前线程内的 ThreadLocalMap 对象
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();
}
/**
*返回当前线程对应的初始值 延时加载
*/
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;
}
/**
* 获取当前线程的ThreadLocalMap,将当前ThreadLocal对象作为key,存储信息作为value存储进map
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap源码
成员变量
/**
* 初始容量 —— 必须是2的冥
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table,数组长度必须是2的冥
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子
* The number of entries in the table.
*/
private int size = 0;
/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容。
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* 扩容的阙值设置为数组长度的2/3
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
存储结构——Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收。
4、 ThreadLocal使用注意点
4.1 内存泄漏问题
ThreadLocal可能引起内存泄漏问题,ThreadLocalMap中Entry的key是弱引用,在下一次垃圾回收时能够被gc,但是值为强引用,无法被gc,如果线程被复用一直运行则value无法释放,JDK已经考虑到了这个问题在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,从而使value对象可以被回收。
但是如果一个ThreadLocal不再被使用,不再调用set,remove,rehash方法,同时线程不中止,那么调用链一直存在,导致value内存泄漏,阿里规约规定使用ThreadLocal完毕后必须释放。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
4.2 空指针问题
如果没有set直接get会返回null,但是如果存的使包装类,返回的基本类型,会有自动拆箱的过程,会空指针异常。
如下get方法就有空指针异常可能
public class UserContextHolder {
public static final ThreadLocal<Long> USER_HOLDER = new InheritableThreadLocal<>();
public static void setUserId(Long userId){
USER_HOLDER.set(userId);
}
public static long getUserId(){
return USER_HOLDER.get();
}
public static void remove(){
USER_HOLDER.remove();
}
}
4.3 共享对象问题
如果ThreadLoacl存放的本身就是共享对象,如static对象,那么多线程ThreadLocal.get()还是获取的这个共享对象本身,依然存在并发访问问题。
5、Spring中的ThreadLocal实例
DateTimeContextHolder
RequestContextHolder