ThreadLocal
ThreadLocal,是一个线程内部的数据存储类,每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。
ThreadLocal与synchronized的比较
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
ThreadLocal是线程局部变量,是一种多线程间并发访问变量的解决方案。和synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的方式,为每个线程提供变量的独立副本,以保证线程的安全。
举个例子
public class ThreadLocalTestDemo {
public static void main(String[] args){
ThreadLocal<String> threadLocal = new ThreadLocal<>();
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("==第一个线程==");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
System.out.println(threadLocal.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("==第二个线程==");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
System.out.println(threadLocal.get());
}
}).start();
}
}
运行结果:
通过测试类我们可以看出,在不同的线程中,访问的是同一个ThreadLocal对象,但是获取的值却是不一样的。
分析ThreadLocal原理
万事撸源码。说多了都是废话,直接看源码
ThreadLocal中全部的方法和内部类结构如下:
我们要弄清楚ThreadLocal是如何做到每一个线程维护一个变量的,那就必须先弄清楚
ThreadLocal.ThreadLocalMap这个内部类
/**
*ThreadLocalMap是自定义的哈希映射,仅适用于维护线程局部值。
*没有在ThreadLocal类之外导出任何操作。该类是包私有的,以允许在Thread类中声明
*字段。为了帮助处理非常大且长期存在的用法,哈希表条目使用WeakReferences作为键。
*但是,由于未使用引用队列,因此仅在表开始空间不足时,才保证删除过时的条目
*/
static class ThreadLocalMap {
/**
*此哈希映射中的条目使用其主要引用字段作为键(始终是ThreadLocal对象)
*扩展了WeakReference。请注意,空键(即entry.get()== null)
*意味着不再引用该键,因此可以从表中删除条目。
*在下面的代码中,此类条目被称为“陈旧条目”。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] 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
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
Entry是一个包含key和value的一个对象,ThreadLocal<?>为key,要保存的线程局部变量的值为value,通过super(k);调用 WeakReference的构造函数, 将ThreadLocal对象变成一个弱引用的对象,这样做是为了在线程销毁的时候,对应的实体会被回收,不会出现内存泄漏
Thread和ThreadLocalMap的关系
看下图,不难看出Thread中的threadLocals对应的就是ThreadLocal中的ThreadLocalMap:
简单来说:每一个Thread中都保存着自己的一个ThreadLocalMap,一个ThreadLoaclMap中可以有多个ThreadLocal对象,也可以说同一个线程下不同的ThreadLocal对象共用一个ThreadLocalMap ,其中一个ThreadLocal对象对应着map中的一个Entry(即ThreadLocalMap的key是ThreadLocal的对象,value是独享数据)
下图可以更好的理解:
思考:为什么用ThreadLocalMap来保存线程的局部对象(即ThreadLocal)?
因为一个线程所拥有的局部对象可能会很多,会有很多个ThreadLocal,这样的话,不管一个线程拥有多少个局部变量,都用一个ThreadLocalMap来保存,我们来看map.set(this,value)。并且会自动扩容;
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();
}
可以看出ThreadLocal 所操作的是当前线程的 ThreadLocalMap 对象中的 table 数组,并把操作的 ThreadLocal 作为键存储
ThreadLocal的set方法
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,所以set的时候先获取当前线程的map对象,如果map是null,说明第一次添加,会进行创建createMap(t,value)
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
初始化TreadLocalMap中的table的初始值是16,超过容量2/3的时候下一次再像map中set数据的时候会扩容
ThreadLocal的get方法
public T get() {
Thread t = Thread.currentThread();
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;
}
protected T initialValue() {
return null;
}
在调用get拿数据的时候,首先拿到当前线程中的map,判断是否为null。如果是空的,会调用setInitialValue(),这里的value一定是null。如果map不为null,调用getEntry(this)方法,传入当前的ThreadLocal。
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);
}
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;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
通过哈希计算得出下标,然后比较当前参数key(TreadLocal)和map中存储的TreadLocal是不是一个,如果不是的话就调用getEntryAfterMiss()开始线性查找,
一旦碰到空对象就停止返回,这里expungeStaleEntry()方法也对内存泄漏做了优化操作。即对key=null 的key所对应的value进行赋空, 释放了空间避免内存泄漏。同时它遍历下一个key为空的entry, 并将value赋值为null, 等待下次GC释放掉其空间
我们发现,set()和get()方法都会调用该方法。
ThreadLocal可能引起的OOM内存溢出问题简要分析
刚刚我们也说了,ThreadLocal可能导致内存泄漏,那么具体的原因是为什么呢?
因为ThreadLocal的原理是操作Thread内部的一个ThreadLocalMap,这个Map的Entry继承了WeakReference,即Entry中的key是弱引用。java中的弱引用会在下次GC的时候会被回收掉,所以key会被回收,但是value并不会被回收掉。这样导致key为null,value有值。线程如果销毁,value也会被回收,但是如果在线程池中,线程执行完之后是返回线程池中,并不是销毁,同时GC的时候把key清除了,那么这个value永远不会被清除,久而久之就会内存溢出。所以jdk开发者针对这一情况也做了优化。也就是我们上面说的expungeStaleEntry()这个方法。但是这样做也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题。比如极端情况下我们只创建ThreadLocal但不调用set、get、remove方法等。
怎么解决这个内存泄漏问题
JDK本身的优化:
set和get方法做了key==null的擦除value的操作 expungeStaleEntry(i);
开发者优化:
每次使用完ThreadLocal都调用它的remove()方法清除数据。因为它的remove方法会主动将当前的key和value(Entry)进行清除。
弱引用如果有内存泄漏危险,那为什么key不设置为强引用
强引用更不行,因为如果key是强引用,当TreadLocal对象要被回收时。但是TreadLocalMap中依然保持这个ThreadLocal对象的强引用,而ThreadLocalMap又被当前线程Thread强引用,也就是说当线程不销毁的时候,ThreadLocalMap就不会被回收,从而导致ThreadLocal也不会被回收,除非手动删除key
弱引用会在下一次GC的时候强制回收。虽然也会导致内存溢出,但是最起码也有set、get、removede方法操作对null key进行擦除的补救措施, 方案上略胜一筹。
总结
(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;
(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;
(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;
(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;
(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);
(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;
(7)线程死亡时,线程局部变量会自动回收内存;
(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;
(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
应用
ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
spring中ThreadLocal的应用
ThreadLocal天生为解决相同变量的访问冲突问题, 所以这个对于spring的默认单例bean的多线程访问是一个完美的解决方案。spring也确实是用了ThreadLocal来处理多线程下相同变量并发的线程安全问题。
spring 如何保证数据库事务在同一个连接下执行的
要想实现jdbc事务, 就必须是在同一个连接对象中操作, 多个连接下事务就会不可控, 需要借助分布式事务完成。那spring 如何保证数据库事务在同一个连接下执行的呢?
DataSourceTransactionManager 是spring的数据源事务管理器, 它会在你调用getConnection()的时候从数据库连接池中获取一个connection, 然后将其与ThreadLocal绑定, 事务完成后解除绑定。这样就保证了事务在同一连接下完成。