联系:ThreadLocal和Synchronized都是为解决多线程对相同数据访问冲突的问题。
区别:
① Synchronized采用同步锁机制,使变量或代码块在同一时间只能被一个线程访问,采取的是“以时间换空间”的方式;
ThreadLocal为每一个线程提供一份变量副本,使线程并行访问时操作数据互不影响,采取的是“以空间换时间”的方式;
② Synchronized保护的是多线程的共享变量,每一个线程对该变量的操作都会影响变量的最终结果,
例如多线程操作火车票总数,每一个线程的操作都会影响最终的票数;
ThreadLocal用于处理跟最终结果无关的变量,例如有一个静态变量
public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
如果多个线程同时调用sdf.format(...),有可能导致sdf使用的内部数据结构被破坏,所以为每一个线程提供一个sdf的副本,每个线程使用各种的sdf,而使用多个sdf对最终结果并没什么影响。
public static final ThreadLocal threadLocal = new
ThreadLocal() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
在程序中定义一个ThreadLocal类型的对象,使用变量threadLocal指向该对象,每个线程访问时,都会生成一个threadLocal对象副本,即使该线程调用多次方法,返回的还是同一个局部threadLocal对象。
SimpleDateFormat sdf = threadLocal.get();
ThreadLocal的get()方法,就是从当前线程的ThreadLocalMap中取出当前线程对应的变量副本。该Map的key是ThreadLocal对象即上述threadLocal,value是当前线程对应的变量,即所需的sdf。
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
当第一次调用get()方法时,threadLocals变量尚未初始化,故get()方法会调用setInitalValue()方法
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;
}
由于我们自己重写了initialValue()方法,故value值变为SimpleDateFormat对象,ThreadLocalMap没有被初始化的话,便初始化,并在构造时设置firstKey和firstValue;如果已经被初始化,那么将key和value存入map中。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
注意:变量threadLocals是保存在线程Thread中的,而不是保存在ThreadLocal中。当前线程Thread中,有一个变量,引用名threadLocals,类型为ThreadLocal.ThreadLocalMap,这个引用是在ThreadLocal类中createmap方法内初始化的。
每个线程都有这样一个名为threadLocals的ThreadLocalMap,以ThreadLocal(即threadLocal)和ThreadLocal对象泛型类型声明的变量(即SimpleDateFormat)作为key和value,这样,我们所使用的ThreadLocal变量的实际数据,通过get()方法取值的时候,就是通过取出当前Thread中threadLocals引用的map,然后从这个map中根据当前ThreadLocal作为参数,取出数据。现在,变量的副本从哪里取出的得到解决。
static class ThreadLocalMap {
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
}
它也是一个类似于HashMap的数据结构,但是并没有实现Map接口。
也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,相应的线程被称为这些Entry的属主线程;只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set()方法,把ThreadLocal对象自己当做key,存入ThreadLocalMap中。
ThreadLocalMap的Entry继承了WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
// 表的大小始终为 2 的幂次
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;
}
在ThreadLocalMap中,形如 key.threadLocalHashCode & (table.length - 1)(其中 key 为一个 ThreadLocal 实例)这样的代码片段实质上就是在求一个 ThreadLocal 对象的哈希值,只是在源码实现中没有将其抽为一个公用函数。
对于& (INITIAL_CAPACITY - 1),相对于 2 的幂作为模数取模,可以用&(2^n-1)来替代%2^n,位运算比取模效率高很多。至于为什么,因为对 2^n 取模,只要不是第n 位对结果的贡献显然都是 0,会影响结果的只能是第 n 位。
ThreadLocalMap是何时初始化的,上面有提及,是在get()方法的最后一行调用了setInitialValue()方法,而setInitialValue()中又调用createMap()方法对ThreadLocalMap进行初始化。
Entry的key是一个ThreadLocal实例,value是一个线程特有对象。Entry的作用即是:为其属主线程建立起一个ThreadLocal实例与一个线程特有对象之间的对应关系;
因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四挡引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着他被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
如果key设置为强引用,当threadLocal实例释放后,threadLocal=null,但是Thread会有强引用指向threadLocalMap,而我们知道threadLocalMap中的key为threadLocal,那么threadLocalMap.Entry又强引用threadLocal,这样会导致threadLocal不能正常被GC回收。弱引用虽然会引起内存泄漏,但是有set()、get()、remove()等方法做补偿,对为null的key进行擦除,设计略胜一筹。
事实上,当currentThread执行结束后,threadLocalMap变得不可达而被回收,Entry等也被回收了,但这样就要求不对Thread进行复用,但是我们项目中经常会复用线程来提高性能,所以currentThread一般不会处于中止状态。
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
// 因为用的是线性探测,所以往后找还是有可能能够找到目标 Entry 的。
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)
// 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的 entry
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
i 是位置
从 staleSlot 开始遍历,将无效 key(弱引用指向对象被回收)清理,即对应 Entry 中的 value置为 null,将指向这个 Entry 的 table[i]置为 null,直到遍历完 Entry。
另外,在过程中还会对非空的 Entry 作 rehash。
可以说这个函数的作用就是从 staleSlot 开始清理连续段中的 slot,断开value的强引用,rehash slot等。
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;
// 遍历下一个key为空的Entry,并将value赋空
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 {
// 对于还没有被回收的情况,需要做一次 rehash。
// 如果对应的 ThreadLocal 的 ID 对 len 取模出来的索引 h 不为当前位置 i,
// 则从 h 向后线性探测到第一个空的 slot,把当前的 entry 给挪过去。
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
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;
// 计算 key 的 hash 值
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) {
// 同一个 ThreadLocal 赋了新值,则替换原值为新值
e.value = value;
return;
}
if (k == null) {
// 该位置的 TheadLocal 已经被回收,
// 那么会清理 slot 并在此位置放入当前 key和 value(stale:陈旧的)
replaceStaleEntry(key, value, i);
return;
}
}
// 下一个位置为空,那么就放到该位置上
tab[i] = new Entry(key, value);
int sz = ++size;
// 启发式地清理一些 slot,并判断是否是否需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal对象,hash 值就增加一个固定的大小 0x61c88647。
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
由于 ThreadLocalMap 使用线性探测法来解决散列冲突,所以实际上 Entry[]数组在程序逻辑上是作为一个环形存在的。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
在插入过程中,根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置 i,过程如下:
1、如果当前位置是空的,那么正好,就初始化一个 Entry 对象放在位置 i 上;
2、不巧,位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 正好是即将设置的 key,那么重新设置 Entry 中的 value;
3、很不巧,位置 i 的 Entry 对象,和即将设置的 key 没关系,那么只能找下一个空位置;这样的话,在 get 的时候,也会根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置,然后判断该位置 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位置;
可以发现,set 和 get 如果冲突严重的话,效率很低,因为 ThreadLoalMap 是 Thread 的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。
cleanSomeSlots (启发式地清理 slot )
i 是当前位置,n 是元素个数
i 对应 entry 是非无效(指向的 ThreadLocal 没被回收,或者 entry 本身为空)
n 是用于控制控制扫描次数的
正常情况下如果 log n 次扫描没有发现无效 slot,函数就结束了
但是如果发现了无效的 slot,将 n 置为 table 的长度 len,做一次连续段的清理
再从下一个空的 slot 开始继续扫描
这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效 slot的时候可能会被调用,区别是前者传入的 n 为元素个数,后者为 table 的容量
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
private void rehash() {
// 进行一次全量清理
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
全量清理
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
扩容,因为需要保证 table 的容量 len 为 2 的幂,所以扩容即扩大 2 倍
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;
}
private void remove(ThreadLocal> key) {
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)]) {
if (e.get() == key) {
e.clear(); // 显式断开弱引用,即清除key
expungeStaleEntry(i); // 进行段清理,即清除value
return;
}
}
}
Reference#clear
public void clear() {
this.referent = null;
}
ThreadLocal操作不当会引发内存泄漏,主要原因在于其内部类ThreadLocalMap中Entry的设计。
Entry继承了WeakReference
key为空的话,value是无效数据,久而久之,value的累加就会导致内存泄漏。
JDK 建议将 ThreadLocal 变量定义成 private static 的,这样的话 ThreadLocal 的生命周期就更长,由于一直存在 ThreadLocal 的强引用,所以 ThreadLocal 也就不会被回收,也就能保证任何时候都能根据 ThreadLocal 的弱引用访问到 Entry 的 value 值,然后 remove() 它,防止内存泄露。
而且在一些方法中埋了对key=null的value擦除操作(但也未必会清理所有的需要被回收的 value),这样对应的 value 就没有 GC Roots可达了,下次 GC 的时候就可以被回收,如get()、set()、remove()。
这样做,也只能说尽可能的防止内存泄漏,但不能完全解决内存泄漏问题。比如极端情况下,我们只创建了ThreadLocal,但不调用set()、get()、remove()方法等。所以能解决问题的办法就是用完ThreadLocal之后手动调用remove()方法;
e.clear()用于清除Entry的key,它调用的是WeakReference中的方法:this.referent = null
expungeStaleEntry(i)用于清除Entry对应的value
Thread和ThreadLocal是绑定的,ThreadLocal依赖于Thread去执行,Thread将需要隔离的数据存放到ThreadLocal中(准确的讲是ThreadLocalMap中),来实现多线程数据隔离处理。
这里主要是强化一下手动remove()的思想和必要性,设计思想与连接池类似。
包装其父类remove()方法为静态方法,如果是Spring项目,可以借助bean的生命周期,在拦截器的afterCompletion阶段进行调用。
ThreadLocal天生为解决相同变量的访问冲突问题。所以这个对于Spring的默认单例bean的多线程访问是一个完美的解决方案。Spring也确实是使用了ThreadLocal来处理多线程下相同变量的线程安全问题。
要想实现jdbc事务,就必须是在同一个连接对象中操作,多个连接下的事务就会不可控,需要借助分布式事务完成,那Spring如何保证数据库事务在同一个连接下执行呢?
DataSourceTransactionManager是Spring的数据源事务管理器,它会在你调用getConnection()时从数据库连接池中获取一个connection,然后将其与ThreadLocal绑定,事务完成后解除绑定,这样就保证了事务在同一连接下完成。