ThreadLocal详解
两大使用场景
每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
场景1:每个线程需要一个独享的对象
当只有2个线程时,每个线程可以分别用自己的SimpliDateFormat
当线程延伸到10个时,也可以接受
但当任务为1000 时,必然使用线程池来管理,可是每个任务都会经历SimpliDateFormat对象的创建和销毁
将SimpliDateFormat对象独立出静态对象时,出现了相同时间的问题,线程之间又不安全的问题
使用synchronized加锁的方式可以解决上面的问题
但是又出现了新的问题,上面的操作只是保证了同一时间不可能有多个任务执行这段共享代码,可是这样的话1000个线程运行起来就又要排队了!效率低
更好的解决方案是使用ThreadLocal完美解决问题
例如线程池可以同时运行10个线程,那么就在每个线程的内部创建一个SimpliDateFormat,这样正在运行调用的SimpliDateFormat即是独立的,又保证了安全问题,高效利用内存
代码演示:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalNormalUsage {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal1.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal1=new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
//lambda表达式写法, 作用同上
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
场景2:避免参数传递的麻烦
案例:当前用户信息需要被线程内所有的方法共享
一个比较繁琐的解决方案是把user作为参数层层传递,但是这样会导致代码冗余且不易维护
每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦
当多个线程同时工作时,需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但无论用什么,都会对性能有影响
更好的办法是使用ThreadLocal,在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦
ThreadLocal的两个作用
让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
在任何方法中都可以轻松获取到该对象
根据共享对象的生成时机不同,选择initialValue(在ThreadLocal第一次get时初始化,对象的初始化时机可以由我们控制)或set(生成时间不由我们随意控制,如拦截器生成的用户信息,用ThreadLocal.set直接存放到ThreadLocal中去,方便后续使用)来保存对象
使用ThreadLocal的好处
达到线程安全
不需要加锁,提高执行效率
更高效地利用内存、节省开销(避免每一个任务都创建新对象)
避免参数传递的繁琐,降低代码耦合
Thread的主要方法
initialValue()返回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get的时候才会触发
第一次调用get方法时将调用此方法
每个线程最多调用一次此方法,但如果调用了remove()后再调用get可以再次调用
如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue方法
T initialValue() 初始化
void set(T t) 为这个线程设置一个新值
T get() 得到线程对应的value
void remove() 删除对应这个线程的值
注意点
内存泄漏
某个对象不再拥有,但是占用的内存却不能被回收
Key的泄漏:ThreadLocalMap中的Entry继承自WeakReference,是弱引用(如果这个对象只被弱引用关联,那么这个对象就可以被回收)
Value的泄漏:ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含了一个对value的强引用
正常情况下,当线程终止时,保存在ThreadLocal里的key和value会被垃圾回收,因为没有任何强引用了
但是,如果线程不终止(比如线程要保持很久),那么key对应的value就不能被回收,因为有以下调用链:
Thread -> ThreadLocalMap -> Entry(key为null) -> value
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能出现OOM
jdk会自动扫描key为null的Entry,并把对应的value设置为null
如果一个ThreadLocal不被使用,就可能导致value的内存泄露
如何避免内存泄漏(阿里规约)
调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法。
空指针异常
get方法前如果没有赋值,并且定义的是基本数据类型,但是调用返回的是应该包装类型,所以装箱拆箱就会导致异常,应该改为返回类型是包装类型,因为T initialValue()定义的是泛型返回。