并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。
如果使用锁来保证资源隔离,会存在大量锁等待,会让响应时间延长很多。
ThreadLocal的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。
ThreadLoacl原理
public class ThreadLocal<T> {
...
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();
}
private T setInitialValue() {
//获取要初始化的数据
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为空
if (map != null)
//将初始值设置到map中,key是this,即threadLocal对象,value是初始值
map.set(this, value);
else
//如果map为空,则需要创建新的map对象
createMap(t, value);
return value;
}
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为空
if (map != null)
//将值设置到map中,key是this,即threadLocal对象,value是传入的value值
map.set(this, value);
else
//如果map为空,则需要创建新的map对象
createMap(t, value);
}
static class ThreadLocalMap {
...
}
...
}
ThreadLocal的get方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
private Entry[] table;
...
}
ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。
ThreadLocalMap内部还包含了一个Entry数组,其中:Entry = ThreadLocal + value。
而ThreadLocalMap被定义成了Thread类的成员变量。
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。
ThreadLocalMap的Entry为什么用ThreadLocal做key?
如果一个线程只使用一个ThreadLocal对象,使用Thread做key是可以的。但是一个线程中不可能只使用一个ThreadLocal对象,再使用Thread做key就有问题
Entry的key为什么设计成弱引用?
弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。
如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。
线程很多情况下是复用的,一个线程执行多个任务。一个任务执行完如果不把key设置为nul会一直存在,从而造成内存泄漏。
Entry的value为什么不设计成弱引用?
Entry的value假如只是被Entry引用,有可能没被其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致出现异常。
ThreadLocal真的会导致内存泄露?
强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object
若Thread为工作线程,长期存在,会导致内存泄漏
如何解决内存泄露问题?
在finally代码块中,调用remove方法清理没用的数据,remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。
父子线程如何共享数据?
ThreadLocal都是在一个线程中保存和获取数据的。
@Test
public void test1() {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(6);
System.out.println("父线程获取数据:" + threadLocal.get());
new Thread(() -> {
System.out.println("子线程获取数据:" + threadLocal.get());
}).start();
}
使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。
@Test
public void test2(){
InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
threadLocal.set(6);
System.out.println("父线程获取数据:" + threadLocal.get());
new Thread(() -> {
System.out.println("子线程获取数据:" + threadLocal.get());
}).start();
}
InheritableThreadLocal的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。
线程池中如何共享数据?
public void test3(){
InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
threadLocal.set(6);
System.out.println("父线程获取数据:" + threadLocal.get());
ExecutorService executorService = Executors.newSingleThreadExecutor();
threadLocal.set(6);
executorService.submit(() -> {
System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
});
threadLocal.set(7);
executorService.submit(() -> {
System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
});
}
由于这个例子中使用了单例线程池,固定线程数是1。
第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。
之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却依然是6。
因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。
使用阿里巴巴的一个开源jar包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
@Test
public void test4(){
TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
threadLocal.set(6);
System.out.println("父线程获取数据:" + threadLocal.get());
ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
threadLocal.set(6);
ttlExecutorService.submit(() -> {
System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
});
threadLocal.set(7);
ttlExecutorService.submit(() -> {
System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
});
}
ThreadLocal为什么建议用static修饰?
static修饰的变量是在类在加载时就分配地址了,在类卸载才会被回收
如果变量ThreadLocal是非static的就会造成每次生成实例都要生成不同的ThreadLocal对象,虽然这样程序不会有什么异常,但是会浪费内存资源