在网上见过了太多了关于ThreadLocal的文章,各说各的理,都说是线程安全的,但又说不出哪里体现出线程安全,又说内存异常,又很难解释为什么会造成内存溢出,算了,还是自己看源码研究把,这篇博客就是自己在这个背景下完成的。
由ThreadLocal的特性可知,在同个线程里面,针对同个ThreadLocal对象的赋值,在线程中都是可以根据该ThreadLocal对象获取的,而ThreadLocal同时也是线程私有的,不用担心共享数据的问题。
org.springframework.jdbc.datasource.DataSourceUtils#getConnection
org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection
org.springframework.transaction.support.TransactionSynchronizationManager#getResource
ThreadLocal针对线程的并发问题,是争论得比较多的,ThreadLocal其实无法处理共享数据的多线程并发问题的,它最多只是把共享数据在自己的线程内部拷贝了一个副本而已,针对共享数据的写操作还是会有并发问题的,线程的共享数据的拷贝也无法知道共享数据的最新的值。《阿里巴巴java开发手册》针对ThreadLocal中也有这样的描述:
这句话怎么理解呢?以下面的代码作为示例:
避免某些需要考虑线程安全必须同步带来的性能损失
这句话又该如何理解呢?以SimpleDateFormat
为例,我们大家都知道SimpleDateFormat
是线程不安全的,在多线程并发的时候会出现问题
public class DateHelpUtils{
private final static SimpleDateFormat SDFTIMES = new SimpleDateFormat(
"yyyyMMddHHmmss");
private final static SimpleDateFormat testSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 得到n天之后的日期
*
* @param days
* @return
*/
public static String getAfterDayDate(String days) {
int daysInt = Integer.parseInt(days);
Calendar canlendar = Calendar.getInstance();
canlendar.add(Calendar.DATE, daysInt);
Date date = canlendar.getTime();
//SimpleDateFormat sdfd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = testSdf.format(date);
return dateStr;
}
/**
* 得到几天前的时间 yyyy-MM-dd HH:mm:ss
* @param day
* @return
*/
public static String getDateBeforeTime(int day) throws ParseException {
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, -day);
date = calendar.getTime();
//SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String value = testSdf.format(date);
Date date2 = testSdf.parse(value);
return value;
}
/**
* 几个小时后
* @param hour
* @return
*/
public static String getTimeByHour(int hour) throws ParseException {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY) + hour);
String date = testSdf.format(calendar.getTime());
Date date2 = testSdf.parse(date);
return date;
}
}
@Test
public void testSimpleDateFormatThreadProblem() {
int i = 0;
int j = 0;
int k = 0;
while (i++ < 50) {
System.out.println("i执行:" + i);
new Thread(new Runnable() {
@Override
public void run() {
DateHelpUtils.getAfterDayDate(new Random().nextInt(1000) + "");
}
}).start();
}
while (j++ < 50) {
System.out.println("j执行:" + j);
new Thread(new Runnable() {
@Override
public void run() {
try {
DateHelpUtils.getDateBeforeTime(new Random().nextInt(1000));
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
while (k++ < 50) {
System.out.println("k执行:" + k);
new Thread(new Runnable() {
@Override
public void run() {
try {
DateHelpUtils.getTimeByHour(new Random().nextInt(1000));
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 得到n天之后的日期
*
* @param days
* @return
*/
public synchronized static String getAfterDayDate(String days) {
int daysInt = Integer.parseInt(days);
Calendar canlendar = Calendar.getInstance();
canlendar.add(Calendar.DATE, daysInt);
Date date = canlendar.getTime();
//SimpleDateFormat sdfd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = testSdf.format(date);
return dateStr;
}
/**
* 得到几天前的时间 yyyy-MM-dd HH:mm:ss
* @param day
* @return
*/
public synchronized static String getDateBeforeTime(int day) throws ParseException {
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, -day);
date = calendar.getTime();
//SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String value = testSdf.format(date);
Date date2 = testSdf.parse(value);
return value;
}
/**
* 几个小时后
* @param hour
* @return
*/
public synchronized static String getTimeByHour(int hour) throws ParseException {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY) + hour);
String date = testSdf.format(calendar.getTime());
Date date2 = testSdf.parse(date);
return date;
}
private final static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
/**
* 得到n天之后的日期
*
* @param days
* @return
*/
public static String getAfterDayDate(String days) {
int daysInt = Integer.parseInt(days);
Calendar canlendar = Calendar.getInstance();
canlendar.add(Calendar.DATE, daysInt);
Date date = canlendar.getTime();
//SimpleDateFormat sdfd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.get().format(date);
return dateStr;
}
/**
* 得到几天前的时间 yyyy-MM-dd HH:mm:ss
* @param day
* @return
*/
public static String getDateBeforeTime(int day) throws ParseException {
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, -day);
date = calendar.getTime();
//SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String value = sdf.get().format(date);
Date date2 = sdf.get().parse(value);
return value;
}
/**
* 几个小时后
*kai.wang
* @param hour
* @return
*/
public static String getTimeByHour(int hour) throws ParseException {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY) + hour);
String date = sdf.get().format(calendar.getTime());
Date date2 = sdf.get().parse(date);
return date;
}
通过ThreadLocal的方式进行优化后的代码也能实现线程安全(通过
线程隔离
的方式),能减少因为加锁所带来的性能损失。也许有人会有疑惑,通过ThreadLoal实现的方式,和我通过在方法中实例一个局部SimpleDateFormat变量有什么区别呢?区别就在于,一个线程里面可能会调用多个方法,每个方法中都会调用SimpleDateFormat变量,通过ThreadLocal这个线程里面中不同的方法可以共享同一个SimpleDateFormat变量,如果在方法中实例一个SimpleDateFormat局部变量,在一个线程中只调用一个方法效果是一样的,但是如果在一个线程中调用多个方法,那就要实例化多个SimpleDateFormat变量,最终目的也能达到,但是牺牲了性能和空间。
总的来说,ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法间或类间共享的场景。
This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the {@code ThreadLocal} instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被
private static
修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
每个Thread都有一个
ThreadLocal.ThreadLocalMap
的受保护的对象
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap虽然也叫Map,但是不像
HashMap
一样实现了Map
接口,而是包含了一定数量的Entry
对象,ThreadLocalMap
和HashMap
一样是通过数组定位到Entry
对象,但是和HashMap
不同的是,ThreadLocalMap
没有链表结构,如果发生哈希冲突了,那么就会以index++
的方式往下找到一个可以存储Entry的槽位
## ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();
/***
获取该线程绑定的ThreadLocalMap(即threadLocals),如果ThreadLocalMap在该线程
还未初始化,则初始化一个ThreadLocalMap并赋值threadLocals
***/
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
## ThreadLocakMap的初始化方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建指定长度的Entry数组,默认是16
table = new Entry[INITIAL_CAPACITY];
/***
每个ThreadLocal在初始化的时候都会分配一个HashCode,这个hashCode是通过一个从0开始
自增的整型每次递增0x61c88647实现的
无论hashCode怎么实现,最终的目的都是希望能尽量均匀的分布对象,减少哈希冲突
***/
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//当数量到达Threshold规定的长度时,扩容并reHash
setThreshold(INITIAL_CAPACITY);
}
## ThreadLocalMap的Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
public void test() {
ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();
ThreadLocal<String> threadLocal2 = new ThreadLocal<String>();
threadLocal1.set("threadLocal1");
threadLocal2.set("threadLocal2");
Stirng value1 = threadLocal1.get();
String value2 = threadLocal2.get();
}
以上的代码是ThreadLocal的最简单的用法,通过ThreadLocal对象存储一个字符串,然后又通过该ThreadLocal对象获取到字符串。它和我们之前看到的
key-value
的用法不同的是,它以ThreadLocal对象调用set
方法,然后以调用对象本身作为key
,如果要在同个线程中存储多个,那么就要实例化多个ThreadLocal
对象,如原理图所示。ThreadLocal对象在Entry中是通过一个弱引用指向它的,至于为什么要使用弱引用,我们会在下面讲解到。
在ThreadLocal对象没有调用
set
方法然后直接调用get()
方法会直接返回一个null值,那是因为在ThreadLocal的get方法中,如果发现当前的ThreadLocalMap为空,会调用setInitialValue
private方法,该方法会调用initialValue
protected方法,然后通过initialValue
获取到的值初始化map,并返回initialValue
的值,由于initialValue
方法默认的返回值是null,所以针对一个直接调用get
的threadLocal会返回null。由于setInitialValue
是private方法,我们可以在初始化ThreadLocal
对象的时候通过覆盖initialValue
方法设置初始值,这样在直接调用get
方法的时候,如果当前ThreadLocal对象的map为空,就会使用initialValue
返回值
private final static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
由以上的内容可知,ThreadLocal对象在调用set方法的时候会把自己作为Entry的key,而Entry的key是作为弱引用存在的,至于什么是弱引用,可以参考下图:
对于一个对象来说,我们平时的new方法都是在栈中新建一个对堆中对象的一个强引用,该对象被回收的前提是,GC Root不可达,即没有强引用指向该对象。如果一个对象
仅仅
(注意这里的仅仅
)被弱引用指向,那么在下一次垃圾回收的时候,该对象必定会被回收,弱引用可作为配置文件的初始化的时候使用(个人见解)
行了,说了那么多关于弱引用的,大家对弱引用应该有了一个基本的了解了。要理解ThreadLocalMap的Entry为什么要使用弱引用,我们来想这个问题,如果ThreadLocalMap的Entry不使用弱引用做key会怎样
ThreadLocalMap
是protected
类型的,也就是说我们无法在其包外访问ThreadLocalMap
,针对ThreadLocal
,其开放出来的方法其实是很少的,如果我们无法访问ThreadLoclMap
,其里面的Entry
更是无从访问,所以一旦下面的代码执行,那么该Entry
是无法访问到的,而作为key的ThreadLocal
对象也是无法被回收的
public void test() {
//new ThreadLocal对象有一个强引用threadLocal1
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
//假设threadLocal中的ThreadLocalMap用的是强引用,那么通过set方法,就会有另外个强引用指向new ThreadLocal对象。也就是说现在在栈中有2个强引用指向堆中的ThreadLocal对象
threadLocal.set("threadLocal");
//把外面的强引用置为null,即上面设置的Entry已经没有入口可以访问,但是堆中的ThreadLocal对象无法被回收,因为在Entry内部还有个强引用指向(假设key用的是强引用),那么时间久了之后(假设没手动调用remove方法),势必会造成内存溢出
threadLocal = null;
}
按照上面的结论,Entry用的是ThreadLocal的弱引用,那在外面的ThreadLocal对象不可用的时候,ThreadLocal对象只有一个弱引用指向,那么就会在下次垃圾回收的时候被虚拟机回收。
所以通过上面的反例我们就能更好地理解了为什么ThreadLocal的Entry要用弱引用做key的原因了
由上面内容可知,我们已经通过弱引用解决了Entry的key的内存溢出问题,那么问题来了,Entry的value是强引用,Entry的key被回收后,该Entry已经是没用了,但是Entry的value是存在的,该value无法回收,Entry对象锁占用的内存也是无法回收的,而Entry的value因为对我们来说是不可见,自然就无法回收,所以就存在了内存溢出问题。说到这里,可能有人会问,为什么不把Entry的value也设置为弱引用呢?个人觉得可能value是Object类型的原因吧(这点没有去验证过,有知道的大佬可以评论中知道下,谢谢)
ThreadLocal的set方法和get方法我是在看了半天的源码才基本理解
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();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get
由以上可知,ThreadLocalMap在哈希冲突严重的情况下的执行效率是很低的,因为每次都要遍历整个map
在使用了ThreadLocal之后,下次再调用
set()
和get()
,那么就有可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,但是如果没有再调用set()
或get()
方法,或者说他们没完全清除可被回收对象,那么内存溢出的风险就很大,最后的做法是每次用完ThreadLocal之后手动remove
private final static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public void test() {
try {
threadLocal.set("threadLocal");
} finally{
threadLocal.remove();
}
}