ThreadLocal的功能
在JMM中,所有的对象都存储在堆中,每个线程都能去访问堆中的对象,如果要暂时占有对象,可以使用Synchronized关键字进行加锁操作,但是这种操作只是暂时的,线程释放了锁以后这个对象又会变成共享状态。如果遇到需要生成线程私有变量的场景时,Java提供了一个叫ThreadLocal的类,用来构造线程独享的对象
public class Test {
private static final ThreadLocal<Integer> localInstance = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(final String[] args) {
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 2; i++) {
Integer n = localInstance.get();
localInstance.set(++n);
System.out.println(Thread.currentThread().getName() + " " + n);
}
}
});
t.start();
}
}
}
/*
------输出------
Thread-0 1
Thread-0 2
Thread-1 1
Thread-1 2
Thread-2 1
Thread-2 2
*/
在这个程序运行过程中,尽管有3个线程在对类变量localInstance进行操作,但是每个线程之间的操作是独立的,并没有累加在一起,所以通过ThreadLocal构造的变量是可以保证线程私有的
ThreadLocal的内部实现机制
先来看ThreadLocal最基本的set和get方法
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 然后根据ThreadLocal对象自己找到map中的Entry
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;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
不管是set还是get方法都是对一个ThreadLocalMap的对象进行操作,get时以自身为key去Map中取出变量,set时以自身为key,变量为value存进Map中。这里面的ThreadLocalMap是ThreadLocal的一个内部类,为整个机制的核心,因为每个线程的ThreadLocal变量和其对应的私有变量是以键值对的形式存储在ThreadLocalMap中,所以ThreadLocal相当于在ThreadLocalMap的基础上再封装了一层操作。而获取这个map时要传入当前线程对象是因为每个线程对象都持有一个ThreadLocalMap(threadLocals变量),存储着线程自己的私有变量
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
看到这,线程私有变量实现机制便已经有了答案,前面测试程序中每个线程调用localInstance的get方法时,虽然操作的是同一个类变量,但get方法内部都是去线程自己的ThreadLocalMap中取出来的变量,调用set方法时也是在方法内部把变量存进线程自己的ThreadLocalMap中,这样的话每个线程真正操作的变量自然都是互相独立的
内存泄漏问题
ThreadLocaMap的Entry是继承了WeakReference的,把Map的key声明为了虚引用,虚引用的作用就是当没有强引用关联该对象时如果遇到GC就会被回收,举个简单的例子
String s = new String("string");
WeakReference<String> n = new WeakReference<String>(s);
System.out.println(n.get());
s = null;
System.gc();
System.out.println(n.get());
/*---输出---
string
null
*/
所以当Entry的key没有被强引用关联时就会被垃圾回收,因为ThreadLocalMap生命周期是与线程一致的,如果不再需要使用ThreadLocal对象并且ThreadLocalMap对ThreadLocal对象时强引用的情况下,ThreadLocal对象是不会被回收的,造成了内存泄漏的问题,所以Entry的key被声明为了虚引用,当不再被外部强引用时就会回收
但是这样又产生了新的问题,就是Entry中key被回收了以后,value还是继续存在吗,实践是检验真理的唯一标准,实现一个测试的环境
public class ThreadLocalTest {
private static ThreadLocal<String> localInstance = new ThreadLocal<String>();
public static void main(String[] args) {
String s = new String("string");
WeakReference<ThreadLocal> r = new WeakReference<ThreadLocal>(localInstance);
localInstance.set(s);
while (true) {
Thread t = Thread.currentThread();
s = null;
localInstance = null;
System.gc();
}
}
}
Debug运行,第一轮循环ThreadLocalMap存储着"string"字符串对象,WeakReference对象指向ThreadLocal对象
第二轮循环,WeakReference对象指向空,所以经过上一次GC后ThreadLocal对象已经被回收了,但是ThreadLocalMap还是存储着"string"字符串对象
针对这种情况,其实ThreadLocalMap有实现解决的机制,cleanSomeSlots()方法、expungeStaleEntry()方法和replaceStaleEntry()方法都是为了解决ThreadLocalMap潜在的内存泄漏问题的