ThreadLocal
记录一套比较有参考价值的:
并发容器之ThreadLocal
一篇文章,从源码深入详解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).
大致翻译
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
官方示例:
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal threadId =
new ThreadLocal() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
从官方的说明和例子中可以看出,这个ThreadLocal
是为了给当前线程创建私有化变量副本
的。他创建堆区中的对象(可以被所有线程共享)的副本到Thread Local中给当前线程
使用。也就是只要请求是一个新的线程,那么他在这个请求的任何地方拿目标对象都是该线程私有的
,因为事实上只是以某个对象为原型new了一个出来。总之他的意义在于线程隔离
。
和Thread、ThreadLocalMap关系
ThreadLocal 和 ThreadLocalMap
**ThreadLocalMap
是ThreadLocal的静态内部类
,其内部还有一个继承了WeakReference
的Entry的内部类
下面是官方源码说明:**
The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object). Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, so the entry can be expunged from table. Such entries are referred to as "stale entries" in the code that follows.
一是这个Entry
的实现是继承了WeakReference
(弱引用)的,再一个他的key
就是ThreadLocal本身
。
ThreadLocalMap 和 Thread
ThreadLocalMap是Thread的属性
,正因如此,ThreadLocal才得以实现。
/*
* 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,
* 主要用于父子线程间ThreadLocal变量的传递
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
源码分析
**源码中主要的方法有
set、get、initialValue、withInitial、setInitialValue、remove,下面一个一个分析**
initialValue()
该方法由protect修饰,显然是为了重写
。
**该段说明大意:该方法返回当前线程的ThreadLocal变量的“初始值”。除非在此之前已经调用过set()方法设置值
,否则这个方法会在第一次调用get()获取变量时调用
。正常情况下这个方法只会在第一次调用get()后调用一次
,除非在get()后使用过remove()方法
。
这个实现返回了null
,如果有需要初始值不是null的话,就需要重写该方法
,通常使用匿名内部类
的方式。**
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
get()
该方法用于获取ThreadLocal中的对象,首先拿到当前线程
的线程对象
,通过当前线程对象获取
对应的ThreadLocalMap
。判断map是否为空,然后根据当前的ThreadLocal对象查找对应对象,如果不为空就返回,否则调用setInitialValue()
/**
* 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 = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
setInitialValue()
当get()时没有值
的话,则会调用该方法,该方法会调用initialValue()
设置初始值。并且从这里可以看出,其实在存储的时候不同的是Map,而不是map中的key。在取值的时候是使用象同的key在各个线程中拿到不同的map从而拿到不同的value。
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
### set()
该方法用于给当前线程的LocalThread变量设置值,大多数子类不需要使用这个方法,只依赖于initialValue()方法来设置线程局部变量的值。
/**
* 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);
}
### remove()
移除变量,如果在调用这个方法后再次调用get(),那么会再次执行initialValue()。
/**
* 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 the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
withInitial()
该方法java8引入,为了可以使用lambda来设置初始值,如:
**private static ThreadLocal
threadLocal =
ThreadLocal.withInitial(() ->new
Cpc0010Manager());**
/**
* Creates a thread local variable. The initial value of the variable is
* determined by invoking the {@code get} method on the {@code Supplier}.
*
* @param the type of the thread local's value
* @param supplier the supplier to be used to determine the initial value
* @return a new thread local variable
* @throws NullPointerException if the specified supplier is null
* @since 1.8
*/
public static ThreadLocal withInitial(Supplier extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
可能存在的内存泄露
从下图可以看到,ThreadLocalMap的key对ThreadLocal使用了弱引用
,当ThreadLocal成为null
以后Map中的key会被回收
,但是该Map中的Entry因为有强引用则不会被回收,则造成了key为null
的Entry的存在,从而造成内存泄露。因此源码中为了避免这种情况在set()、get()、remove()时都会清除key为null的Entry。
**使用时应避免ThreadLocal和线程池一起使用,不仅容易OOM而且会造成脏读。
更多可参见:**
get()路线
这里是一个正常的get()方法,当ThreadLocalMap不为空的时候则调用getEntry(this)
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();
}
getEntry()方法如下,这是一个快速命中的策略,直接命中现有key,如果没有命中就会调用getEntryAfterMiss(),这样设计的目的是为了最大限度的提高性能。
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
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);
}
getEntryAfterMiss()是当目标key的hash值没有直接命中而进行后续处理的方法。当key没有命中时会以当前下标开始遍历Entry,并使用expungeStaleEntry()把遇到的key为null的给清除。
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
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;
}
该方法就是将当前key为null的Entry和value也设为null,然后从当前index向后遍历,将key为null的Entry和value也设为null,以便GC。方法返回下一个Entery为null的下标。
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
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为null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
//如果key为null,就设置该Entery和value为null
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//处理hash重复情况
int h = k.threadLocalHashCode & (len - 1);
//如果不是,则设置当前index的Entry为null
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;
}
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);
}
调用map.set(),遍历Map,如果key已经存在则替换,在遍历的同时发现key为null则调用replaceStaleEntry()。否则就新建然后存入,存入时调用cleanSomeSlots(),去除旧Entry。
/**
* 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();
}
replaceStaleEntry()方法用于替换陈旧的Entery。
/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
cleanSomeSlots()方法,主要用于控制清除“脏Entry”的执行,他会现在hash表范围内搜索是否有“脏Entry”,如果有的话就调用expungeStaleEntry()在全表范围内进一步扫描清理。这个方法的设计是为了提高效率。
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
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;
}
测试Demo
共享类
待共享的bean为Cpc0010Manager,则在该类中添加私有静态变量ThreadLocal,并重写initialValue()方法设置初始值,添加通用的get(),set()方法。
public class Cpc0010Manager {
int id;
String name;
private static ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected Cpc0010Manager initialValue(){
Cpc0010Manager cpc0010Manager=new Cpc0010Manager();
cpc0010Manager.id=0;
cpc0010Manager.name="name";
return cpc0010Manager;
}
};
public int getId() {
return id;
}
public Cpc0010Manager setId(int id) {
this.id = id;
return this;
}
public String getName() {
return name;
}
public Cpc0010Manager setName(String name) {
this.name = name;
return this;
}
public Cpc0010Manager getThreadLocal() {
return threadLocal.get();
}
public void setThreadLocal(Cpc0010Manager cpc0010Manager) {
threadLocal.set(cpc0010Manager);
}
}
线程类
添加构造方法,接受Cpc0010Manager的实例,并在run方法中调用。打印的时候分别打印传入Cpc0010Manager的属性和ThreadLocal中的属性。
public class Cpc0010Thread implements Runnable {
Cpc0010Manager cpc0010Manager;
public Cpc0010Thread(Cpc0010Manager manager) {
cpc0010Manager = manager;
}
@Override
public void run() {
System.out.println(Thread.currentThread().toString() + ":" + cpc0010Manager.getThreadLocal() + "---" + cpc0010Manager.getId() + cpc0010Manager.getThreadLocal().getName());
}
}
测试类
测试时分别改变id和name观察变化。sleep是为了保证程序的先后顺序。
public static void main(String[] args) throws InterruptedException {
Cpc0010Manager cpc0010Manager=new Cpc0010Manager();
Cpc0010Thread t1=new Cpc0010Thread(cpc0010Manager);
Cpc0010Thread t2=new Cpc0010Thread(cpc0010Manager);
Cpc0010Thread t3=new Cpc0010Thread(cpc0010Manager);
cpc0010Manager.getThreadLocal();
new Thread(t1).start();
Thread.sleep(1000L);
cpc0010Manager.setId(2);
new Thread(t2).start();
cpc0010Manager.getThreadLocal().setName("newName");
Thread.sleep(1000L);
new Thread(t3).start();
}
结果及小结
控制台输出结果如下:
TThread[Thread-1,5,main]:com.crrcdt.res.config.Cpc0010Manager@1c5bee7d---0localName
Thread[Thread-2,5,main]:com.crrcdt.res.config.Cpc0010Manager@3990d676---2localName
Thread[Thread-3,5,main]:com.crrcdt.res.config.Cpc0010Manager@236e9199---2localName
小结:传入的Cpc0010Manager对象是同一个实例,而从ThreadLocal中获取的都是本地私有的副本,他们的copy的对象的属性是从initialValue()方法而来,除了初始设置的值相同以外,并无其他关联。感悟就是ThreadLocal唯一解决的就是在各个线程中不用自己new的问题。
实际应用
hibernate中的session管理
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
/**
* 升级的MySessionFactory 线程局部模式
* @author xuliugen
*/
public class HibernateUtil {
private static SessionFactory sessionFactory = null;
// 使用线程局部模式
private static ThreadLocal threadLocal = new ThreadLocal();
/*
* 默认的构造函数
*/
private HibernateUtil() {
}
/*
* 静态的代码块
*/
static {
sessionFactory = new Configuration().configure().buildSessionFactory();
}
/*
* 获取全新的的session
*/
public static Session openSession() {
return sessionFactory.openSession();
}
/*
* 获取和线程关联的session
*/
public static Session getCurrentSession() {
Session session = threadLocal.get();
// 判断是是是否得到
if (session == null) {
session = sessionFactory.openSession();
// 把session放到 threadLocal,相当于该session已经于线程绑定
threadLocal.set(session);
}
return session;
}
}
总结
- ThreadLocal为每一个线程提供一个实例,防止互相影响
- ThreadLocal是一个操作类,真正起作用的原因在于Thread中引用了ThreadLocalMap
- ThreadLocal的initialValue()和withInitial()用于设定初始value
- ThreadLocal在调用remov()、get()、set()时会自动清除key为null的entry
- 由于ThreadLocalMap的Entry的key是弱引用,所以在下次gc时,会回收没有被引用的ThreadLocal,而value不会被回收,所以key为null的Entry不会被回收。
- ThreadLocal与线程时一起使用时,要避免内存泄漏和脏读问题
- ThreadLocalMap是ThreadLocal的静态内部类,并使用Entry自行实现了map功能
- ThreadLocalMap使用开放地址法解决hash冲突,因为经常修改
- ThreadLocalMap的初始大小为16,加载因子为2/3,hash表可用大小为16*2/3=10
- 清除“脏Entry”主要通过expungeStaleEntry()、replaceStaleEntry()、cleanSomeSlots()