学习Java中常用的开源框架,Mybatis、Hibernate中设计到线程通过数据库连接对象Connection,对其数据进行操作,都会使用ThreadLocal类来保证Java多线程程序访问和数据库数据的一致性问题。就想深入了解一下ThreadLocal类是怎样确保线程安全的!详解如下:
ThreadLocal的简单使用:
public class Test01 {
ThreadLocal<Long> longLocal=new ThreadLocal<>();
ThreadLocal<String> stringLocal=new ThreadLocal<>();
public void set(){
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong(){
return longLocal.get();
}
public String getString(){
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
new Thread().start();
Test01 t=new Test01();
t.set();
System.gc();
Thread.sleep(100);
System.out.println(t.getLong());
System.out.println(t.getString());
Thread thread1 = new Thread(()->{
t.set();
System.out.println("\n子线程 Thread-0 :");
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getLong());
System.out.println(t.getString());
});
thread1.start();
}
}
//输出结果
//1
//main
//
//子线程 Thread-0 :
//12
//Thread-1
ThreadLocal主要为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。一个线程可以储存多个ThreadLocal的值,具体看上面的例子。
关于ThreadLocal就不看源码了,简单的一批,关键是理解下面几点,就算真正了解了ThreadLocal:
1.每个线程内部有一个Map,类型为ThreadLocal.ThreadLocalMap,(记住是每一个Thread都有一个)
2.对于ThreadLocalMap中的每一个Entry来说,它的key是ThreadLocal,同时这个key为一个弱引用,当ThreadLocal没有强引用指向它的时候,下次垃圾回收必定将他回收,因为value是强引用,所以只要ThreadLocalMap存在不会被回收,又因为ThreadLocalMap是线程的成员变量,所以value会一直被保存到线程被销毁,这就有内存泄漏的问题(LZ当时想,为什么要设计成弱引用,还会出现内存泄漏问题,难道设计成强引用它不香吗?会不会设计JDK啊,不会的话就让我来,不过等后来彻底明白了原理,设计ThreadLocal得大神请收下我的膝盖),
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
正如上图所画,当虚拟机栈中ThreadLocal的强引用ThreadLocalRef不再指向这个ThreadLocal了,他就会被回收。
3.ThreadLocalMap的key为ThreadLocal类型,value为任意对象,注意key是一个ThreadLocal类型,它是一个对象,而且多个线程内的ThreadLocal是共享的(注意这一点很重要,也是理解ThreadLocalMap内存泄漏和为什么使用软引用,ThreadLocal为什么都是定义成private static的关键)
对于不理解的同学,可以看一下上面的例子:
ThreadLocal<Long> longLocal=new ThreadLocal<>();
ThreadLocal<String> stringLocal=new ThreadLocal<>();
对于这两个ThreadLocal,多个线程中的ThreadLocalMap的key都是longLocal和stringLocal对象的引用,可以使用longLocal和stringLocal来查找当前线程自己的值,他们使用的key都是longLocal和stringLocal对象的引用(再强调一下,这时思考如果有线程将longLocal和stringLocal置为了null,这时所有以longLocal和stringLocal对象的引用为key存储的值,还能被取出来吗,这个问题是内存泄漏的关键,如果你把这个问题整清楚了,就知道ThreadLocal为什么使用软引用和内存泄漏问题了)
对于还有点迷糊的同学可以看一下上面这个图,每一个线程里面的ThreadLocalMap中的一个Entry中,key指向的是同一个ThreadLocal的对象引用,一个线程中可以存储有多个Entry,也就是多个ThreadLocal(就像例子里面的一样)。
明白了上面三点,让我们来看一下下面几个问题:
1.ThreadLocal中为什么ThreadLocalMap中的key是软引用?
首先明白一点,在只有被软引用所指向的对象,下次GC时这个对象必定会被回收。
我们假设key使用的是强引用,想要拿到存储在自身线程值的时候,需要以ThreadLocal作为key去取,也就是调用longLocal.get(),它会得到当前线程的ThreadLocalMap,以longLocal这个ThreadLocal对象本身作为key去查找相应的值,还是看一下ThreadLocal的get方法:
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();
}
明白了这一点我们思考,如果此时将longLocal置为null,这样的话在所有线程内你就不能调用longLocal.get()获取值,这样就这个longLocal你就不能再使用了,因为你能使用到的引用已经没有指向这个ThreadLocal了,然后这个ThreadLocal对象也不会被回收( 我们已假设key使用的是强引用),因为在ThreadLocalMap中的key还指向这个ThreadLocal对象,然后这个时候还是会出现内存泄漏(惊不惊喜,意不意外!),最后总结出ThreadLocal使用强引用和软引用都会发生内存溢出的问题。
那么为什么JDK的实现者会时候软引用呢?是这样的,软引用保证了在这个ThreadLocal对象在没有强引用的时候,会被回收掉,这个value怎么办呢?JDK实现者做了这样一个方法,在ThreadLocal对象的get,set和remove方法时,会去清除那些key为null的Entry,这样能够保持程序尽可能小的出现内存泄露问题(注意使用软引用能尽量小的保证出现内存泄漏问题),所以JDK选择使用软引用是最正确的选择,在使用ThreadLocal时,对于用不到的值,要尽量remove清除一下。
2.ThreadLocal的内存泄露问题?
关于这个问题,其实明白ThreadLocal为啥用软引用后,这个问题就简单多了,在ThreadLocal对象没有强引用指向的时候,这个ThreadLocal对象对被回收,然而此时value并没有被回收就造成了内存泄漏,看图:
3.为什么ThreadLocal对象最好使用peivate static来修饰?
这个阿里面试的时候问到过,就像下面这样使用
public class Test01 {
private static ThreadLocal<Long> longLocal=new ThreadLocal<>();
private static ThreadLocal<String> stringLocal=new ThreadLocal<>();
public void set(){
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong(){
return longLocal.get();
}
public String getString(){
return stringLocal.get();
}
}
其原因如果上面看懂了,就很容易知道原因了,我就简单说一下,所有线程都会使用一个ThreadLocal对象作为key,若为类的实例对象(也就是不加static的),每一个实例对象的ThreadLocal都是不一样的。