ThreadLocal实现原理
ThreadLocal是什么
ThreadLocal是⼀个本地线程副本变量⼯具类。主要⽤于将私有线程和该线程存放的副本对象做⼀个映射,各个线程之间的变量互不⼲扰,在⾼并发场景下,可以实现⽆状态的调⽤,特别适⽤于各个线程依赖不通的变量值完成操作的场景。
ThreadLocal和Synchonized都⽤于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。Synchronized⽤于线程间的数据共享,⽽ThreadLocal则⽤于线程间的数据隔离。Synchronized是利⽤锁的机制,使变量或代码块在某⼀时该只能被⼀个线程访问。⽽ThreadLocal为每⼀个线程都提供了变量的副本,使得每个线程在某⼀时间访问到的并不是同⼀个对象,这样就隔离了多个线程对数据的数据共享。⽽Synchronized却正好相反,它⽤于在多个线程间通信时能够获得数据共享。
从数据结构⼊⼿
下图为ThreadLocal的内部结构图
下图为ThreadLocal的堆栈结构
ThreadLocal结构内部
从上⾯的结构图,我们已经窥见ThreadLocal的核⼼机制:
每个Thread线程内部都有⼀个Map。
Map⾥⾯存储线程本地对象(key)和线程的变量副本(value)
但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不⼲扰。Thread线程内部的Map在类中描述如下:
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
深⼊解析ThreadLocal
ThreadLocal类提供如下⼏个核⼼⽅法:
public T get()
public void set(T value)
public void initialValue()
public void remove()
get()⽅法⽤于获取当前线程的副本变量值。
set()⽅法⽤于保存当前线程的副本变量值。
initialValue()为当前线程初始副本变量值。
remove()⽅法移除当前前程的副本变量值。
get()⽅法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = Entry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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;
}
步骤:
1.获取当前线程的ThreadLocalMap对象threadLocals
2.从map中获取线程存储的K-V Entry节点。
3.从Entry节点获取存储的Value副本值返回。
4.map为空的话返回初始值null,即线程变量副本为null,在使⽤时需要注意判断NullPointerException。
set()⽅法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
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;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
步骤:
1.获取当前线程的成员变量map
2.map⾮空,则重新将ThreadLocal和新的value副本放⼊到map中。
3.map空,则对线程的成员变量ThreadLocalMap进⾏初始化创建,并将ThreadLocal和value副本放⼊map中。
remove()⽅法
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of theresized
* <tt>initialValue</tt> method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
remove⽅法⽐较简单,不做赘述。
ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接⼝,⽤独⽴的⽅式实现了Map的功能,其内部的Entry也独⽴实现。
ThreadLocalMap类图
在ThreadLocalMap中,也是⽤Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被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(弱引⽤,⽣命周期只能存活到下次GC前),但只有Key是弱引⽤类型的,Value并⾮弱引⽤。ThreadLocalMap的成员变量:
static class ThreadLocalMap {
/**
* 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
}
Hash冲突怎么解决
和HashMap的最⼤的不同在于,ThreadLocalMap结构⾮常简单,没有next引⽤,也就是说ThreadLocalMap中解决Hash冲突的⽅式并⾮链表的⽅式,⽽是采⽤线性探测的⽅式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占⽤,则利⽤固定的算法寻⼀定步长的下个位置,依次判断,直⾄到能够存放的位置。
ThreadLocalMap解决Hash冲突的⽅式就是简单的步长加1或减1,寻下⼀个相邻的位置。
/
**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采⽤线性探测的⽅式解决Hash冲突的效率很低,如果有⼤量不同的ThreadLocal对象放⼊map中时发送冲突,或者发⽣⼆次冲突,则效率很低。
所以这⾥引出的良好建议是:每个线程只存⼀个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果⼀个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放⼊Map中时会极⼤的增加Hash冲突的可能。
由于ThreadLocalMap的key是弱引⽤,⽽Value是强引⽤。这就导致了⼀个问题,ThreadLocal在没有外部对象强引⽤时,发⽣GC时弱引⽤Key会被回收,⽽Value不会回收,如果创建ThreadLocal的线程⼀直持续运⾏,那么这个Entry对象中的value就有可能⼀直得不到回收,发⽣内存泄露。
如何避免泄漏
既然Key是弱引⽤,那么我们要做的事,就是在调⽤ThreadLocal的get()、set()⽅法时完成后再调⽤remove⽅法,将Entry节点和Map的引⽤关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使⽤ThreadLocal的set⽅法之后,没有显⽰的调⽤remove⽅法,就有可能发⽣内存泄露,所以养成
良好的编程习惯⼗分重要,使⽤完ThreadLocal之后,记得调⽤remove⽅法。
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
threadLocal.set(new Session(1, "Misout的博客"));
// 其它业务逻辑
} finally {
}
应⽤场景
还记得Hibernate的session获取场景吗?
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
/
/获取Session
public static Session getCurrentSession(){
Session session = ();
//判断Session是否为空,如果为空,将创建⼀个session,并设置到本地线程变量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();// 创建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
}
return session;
}
为什么?每个线程访问数据库都应当是⼀个独⽴的Session会话,如果多个线程共享同⼀个Session会话,有可能其他线程关闭连接了,当前线程再执⾏提交时就会出现会话已关闭的异常,导致系统异常。此⽅式能避免线程争抢Session,提⾼并发下的安全性。
使⽤ThreadLocal的典型场景正如上⾯的数据库连接管理,线程会话管理等场景,只适⽤于独⽴变量副本的情况,如果变量为全局共享的,则不适⽤在⾼并发下使⽤。
总结
每个ThreadLocal只能保存⼀个变量副本,如果想要上线⼀个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
ThreadLocal内部的ThreadLocalMap键为弱引⽤,会有内存泄漏的风险。
适⽤于⽆状态,副本变量独⽴后不影响业务逻辑的⾼并发场景。如果如果业务逻辑强依赖于副本变量,则不适合⽤ThreadLocal解决,需要另寻解决⽅案。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论