ThreadLocal是Java中的线程局部变量
官方解释如下
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)
意思是每一个线程都有自己的独立初始化的变量副本。
ThreadLocal可以看作是一个容器,容器里面存放了属于当前线程的变量。
ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:
(1) void set(Object value)设置当前线程的线程局部变量的值。
(2) public Object get()该方法返回当前线程所对应的线程局部变量。
(3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
(4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。
可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量
例子:
- public class TestNum {
- // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
- private static ThreadLocal
seqNum = new ThreadLocal() { - public Integer initialValue() {
- return 0;
- }
- };
- // ②获取下一个序列值
- public int getNextNum() {
- seqNum.set(seqNum.get() + 1);
- return seqNum.get();
- }
- public static void main(String[] args) {
- TestNum sn = new TestNum();
- // ③ 3个线程共享sn,各自产生序列号
- TestClient t1 = new TestClient(sn);
- TestClient t2 = new TestClient(sn);
- TestClient t3 = new TestClient(sn);
- t1.start();
- t2.start();
- t3.start();
- }
- private static class TestClient extends Thread {
- private TestNum sn;
- public TestClient(TestNum sn) {
- this.sn = sn;
- }
- public void run() {
- for (int i = 0; i < 3; i++) {
- // ④每个线程打出3个序列值
- System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
- + sn.getNextNum() + "]");
- }
- }
- }
- }
代码运行结果
thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]
可以看出,变量的值在各个线程之间是独立的,不会互相干扰。这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。
其实在Thread类中维护了一个ThreadLocalMap对象threadLocals(ThreadLocalMap是ThreadLocal类中的一个静态内部类),该Map的Key是当前ThreadLocal对象,Value是对应线程的局部变量副本,每个线程可能存在多个ThreadLocal
ThreadLocal的部分get/set代码如下
- /**
- * 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)
- return (T)e.value;
- }
- return setInitialValue();
- }
- /**
- * Get the map associated with a ThreadLocal. Overridden in
- * InheritableThreadLocal.
- *
- * @param t the current thread
- * @return the map
- */
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- /**
- * 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);
- }
ThreadLocal 是否会存在内存泄漏呢?
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
在JDK的ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,下面是ThreadLocalMap的getEntry
方法的源码:
- 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;
- }
整理一下ThreadLocalMap的getEntry
函数的流程:
在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set
操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的genEntry
函数或者set
函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove
函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static
的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
线程上下文对象,用作存放线程局部变量,为每个线程存放一些特定的数据
1、编写ContextManager,实现线程上下文统一管理。
a、声明ThreadLocal对象
Context为具体的局部变量对象
private static final ThreadLocal
b、编写设置上下文对象的方法bindContext(args...)
在此方法中设置上下文局部变量的数据,并通过setContext()(自编)方法放入到ThreadLocal对象中。
c、编写获取上下文对象(getContext())、设置上下文对象(setContext(Context context))、清理上下文对象(clearContext())的方法,在对应的方法中直接调用ThreadLocal对象自带的对应方法(get()、set()、remove())即可。
之后,在合适的地方直接使用ContextManager对象及其中的方法即可。