1. ThreadLocal的两大使用场景
- 每个线程需要一个独享的对象(通常是工具类),每个线程内有自己的实例副本,不共享;
- 每个线程内需要保持全局变量,可以让不同的方法直接使用,避免传递参数的麻烦;
- 总之,就是解决多个线程的共享变量的线程安全问题;
2. 解决SimpleDateFormat的线程安全问题
- 问题代码:
public class MyThreadLocal {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static String date(int seconds) {
Date date = new Date(1000 * seconds);
return sdf.format(date);
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
executorService.shutdown();
}
}
1970-01-01 08:15:22
1970-01-01 08:15:19
1970-01-01 08:15:19
由结果可以看出,打印出了两个相同的时间,说明发生了运行结果错误,问题代码就发生在sdf.format(date),这行代码不是线程安全的。
- 用synchronized加锁解决
public class MyThreadLocal {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static String date(int seconds) {
Date date = new Date(1000 * seconds);
String s = null;
synchronized (MyThreadLocal.class) {
s = sdf.format(date);
}
// Date date = new Date(1000 * seconds);
return s;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
executorService.shutdown();
}
}
- 用ThreadLocal解决
public class MyThreadLocal1 {
// private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
public static String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat sdf = simpleDateFormatThreadLocal.get();
return sdf.format(date);
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
executorService.shutdown();
}
}
synchronized加锁的方法可以解决线程安全问题,但是由于同一时刻只有一个线程执行,所以效率低下,ThreadLocal方法每个线程内都有自己独享的对象,也不会有线程安全问题。
3. 每个线程内获取全局变量,可以让不同的方法直接使用,避免传递参数的麻烦
public class MyThreadLocal2 {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void service1() {
threadLocal.set("用戶A");
}
public void service2() {
System.out.println(threadLocal.get());
}
public static void main(String[] args) {
MyThreadLocal2 myThreadLocal2 = new MyThreadLocal2();
myThreadLocal2.service1();
myThreadLocal2.service2();
}
}
用戶A
4. ThreadLocal总结
作用:
- 让对象在线程之间隔离;
- 在任何方法中都可以直接获取到对象;
场景:
- 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机由我们控制;
- 保存在ThreadLocal里对象的生成时机不由我们控制,我们用set方法放进去,再用get方法取出来;
好处:
- 线程安全;
- 不需要加锁;
- 高效利用内存;
- 避免传参的麻烦;
5. ThreadLocal原理
- 每一个Thread里面都有一个ThreadLocalMap类型的threadlocals成员变量,它可以存储很多的ThreadLocal对象,因为一个线程可能有多个ThreadLocal对象,其中对象引用名称作为key;
- ThreadLocalMap:也就是Thread.threadLocals,是Thread里的一个成员变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对;键:这个ThreadLocal;值:实际需要的成员变量;
6. 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里面的静态内部类ThreadLocalMap中的静态内部类Entry,里面存储ThreadLocal对象作为key,我们定义的成员变量作为value,所以这个Entry就是kv的键值对。
在构造函数赋值的时候,super(k)继承了WeakReference,k的赋值是个弱引用。
什么是弱引用?
如果这个对象只被弱引用关联,那么这个对象就可以被回收,所以弱引用不会阻止GC,这就是弱引用的好处。
但是value是强引用,只要强引用在,则没法GC,就可能导致内存泄漏。正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了,但是如果线程不终止(比如线程需要保持很久),发生GC时,key被回收了,key变成了null,key对应的value是强引用而不能被回收,因为有以下的调用链:
Thread->ThreadLocalMap->Entry(key=null)->Value
比如使用线程池,线程池里面的线程始终是存在的,而且value和Thread之间存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。
所以最终原因是:ThreadLocalMap的生命周期和Thread一样长,只要Thread不结束,那么ThreadLocalMap中的Value就一直存在于内存中,即使发生GC时,key被回收了,但是value还存在。
如何避免内存泄漏: 当使用完了对应的ThreadLocal,主动调用remove方法删除
7. ThreadLocal总结
- 如果每个线程中set进去的东西本来就是多线程共享的对象,比如static对象,那么ThreadLocal.get的还是共享对象本身,还有线程安全问题;
- 优先使用框架的支持,而不是自己创造,例如在Spring中,如果可以使用RequestContextHolder,那么久不需要自己去维护ThreadLocal,因为自己可能会忘记调用remove方法,造成内存泄漏;
- 每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景;