1、从简单层面理解
ThreadLocal是绑定在每一个线程上的本地对象,线程独立拥有,线程存在,ThreadLocal就一直存在。比如我们new了一个ThreadLocal对象,此时有两个线程A和B,在A线程里set一个一个对象到ThreadLocal里面,过2sB线程去读取是读取不到的读到的是null,因为ThreadLocal是线程独立拥有的,只能自己拿到别的线程拿不到。
测试代码如下:
public static void main(String[] args) {
ThreadLocal<Person> t = new ThreadLocal<Person>();
// A线程
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.set(new Person(1L, "测试"));
System.out.println(t.get());
}).start();
// B线程
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.get());
}).start();
}
@AllArgsConstructor
@Data
static class Person{
private Long id;
private String name;
}
2、开发中的典型应用
我们开发常用的事务注解@transactional就是利用ThreadLocal的原理,保证当前线程里多个方法拿到的数据库连接都是同一个连接
3、从源码角度看底层实现
public static void main(String[] args) {
ThreadLocal<Person> t = new ThreadLocal<Person>();
t.set(new Person(1L, "测试"));
t.remove();
}
@AllArgsConstructor
@Data
static class Person{
private Long id;
private String name;
}
我们首先new了一个ThreadLocal对象,然后将Person对象set进去,我们点击set方法进去看:
可以看到这个set方法里面的实现是先拿到当前线程,然后调用getMap(t)方法
而这个getMap方法的实现又是调用的当前线程的成员变量threadLocals方法直接得到的。
至此,我们知道了map的由来,再去看map.set(this, value)这个方法是将ThreadLocal对象作为key,set的具体对象值作为value存进map中,然后我们在点击map.set()方法进去看下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();
}
可以看到map.set里面具体的做法是将key和value放到一个Entry对象中,这个entry其实就可以看做是map的一条记录行,我们在点击Entry进去看:
可以看到Entry对象是继承WeakReference弱引用对象的,并且它的构造器中是调用父类中的key的,即放到WeakReference对象中的ThreadLocal对象。
好了,所有源码看完,我们可以得出结论:
我们new出的任何线程Thread对象,对象里有一个成员变量,这个成员变量指向一个map,最开始是null值,当我们new一个ThreadLocal对象出来,然后调用它的set方法,那么这个方法就会把ThreadLocal<T>对象作为key,set的对象做为value存进map中。map的set方法里面具体是把key和value放到一个new的Entry里面,这个entry其实就可以看作map的一条记录行,并且这个entry继承了WeakReference弱引用,以及它的构造器是调用了父类的方法,这个方法是我们new WeakReference时放的对象,即上面说的key。所以map里面的key时通过一个弱引用指向的Thread Local对象。
4、分析ThreadLocal防止内存泄漏的问题
从上面的源码我们可以看到:
问题1:我们一开始new了一个ThreadLocal对象是强引用指向的,但是在往map里面set的时候,又有一个弱引用指向ThreadLocal对象,这是为什么?为什么要使用弱引用?
是为了防止内存泄露,如果map里面的set也用强引用指向ThreadLocal,那么这样GC的时候即使我们外面一开始强引用的对象不再被引用了,但是map里面的还在引用无法回收,而弱引用遇到GC会直接被回收。
问题2:当ThreadLocal对象被回收后,map里面的key就为null了,那么map里面的这条记录就没有意义了,但是垃圾回收是不会回收这个的,需要我们手动删除,所以在我们不用的时候需要加上remove(看上面的代码)不然也容易产生内存泄漏。