ThreadLoacl
应用场景
- 线程单例:每个线程需要独享单个对象,通常是工具类,比如
SimpleDateFormat
和Random
- 线程独享参数提纯:每个线程内需要保存全局变量,让不同方法直接使用,避免传参的麻烦
作用与好处:让需要使用到的对象在线程之间隔离,无需加锁达到线程安全的目的;且在方法中轻松获取,避免传参的麻烦,达到解耦的目的;从而提高了程序的执行效率;在某种意义上节省了内存开销,比如线程不安全的工具类
线程单例
public class Main {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Set<String> set = new HashSet<>();
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(() -> {
set.add(formatData(finalI * 1000));
});
}
executorService.shutdown(); //任务执行完后关闭线程池
if (executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MINUTES)) {
System.out.println(set.size()); //线程池任务都执行结束后查看set中的元素个数
}
}
public static String formatData(long timestamp) {
Date date = new Date(timestamp);
//return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
//工具类对象只需要一个,每次new对象导致浪费内存和GC
//return simpleDateFormat.format(date);
//公用一个工具类对象,但是由于该工具类是线程不安全的,所以会有重复的值
//synchronized (Main.class) {
//return simpleDateFormat.format(date);
//使用同步方式可解决共用一个工具类对象的线程不安全问题,但是同步机制需要线程排队效率较低
//}
return simpleDateFormatThreadLocal.get().format(date);
//使用ThreadLocal工具类,既保证线程安全又保证效率
}
static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<>() { //使用withInitial()静态方法即使用lambda表达式
@Override
protected SimpleDateFormat initialValue() { //设置初始值
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
线程独享参数提纯
public class Main {
public static void main(String[] args) {
//new Service1().process("张三"); //层层参数传递,耦合度高
new Service1().processThreadLocal("张三");
}
}
class UserNameContextHolder {
public static ThreadLocal<String> holder = new ThreadLocal<>(); //由于不知道何时进行初始化值,所以需要使用set()方法设置
//若使用map集合,会有线程安全问题
//若使用线程安全的map集合,会有性能影响
}
class Service1 {
public void process(String userName) {
System.out.println("Service1设置用户名:" + userName);
new Service2().process(userName);
}
public void processThreadLocal(String userName) {
UserNameContextHolder.holder.set(userName);
System.out.println("Service1设置用户名:" + userName);
new Service2().processThreadLocal(); //进入下一个业务
}
}
class Service2 {
public void process(String userName) {
System.out.println("Service2拿到用户名:" + userName);
new Service3().processThreadLocal(); //进入下一个业务
}
public void processThreadLocal() {
String userName = UserNameContextHolder.holder.get();
System.out.println("Service2拿到用户名:" + userName);
new Service3().processThreadLocal(); //进入下一个业务
}
}
class Service3 {
public void process(String userName) {
System.out.println("Service2拿到用户名:" + userName);
new Service3().processThreadLocal(); //进入下一个业务
}
public void processThreadLocal() {
String userName = UserNameContextHolder.holder.get();
System.out.println("Service2拿到用户名:" + userName);
}
}
ThreadLoacl原理
在每个Thread
类中都有一个ThreadLocalMap
的成员变量,该属性存储着该线程中若干个ThreadLocal
对象
ThreadLocal
T initialVlaue()
:该方法会返回当前线程对应ThreadLocal
初始值- 若不重写该方法,该方法默认返回null
- 是一个懒加载的方法,只有在第一次调用的是
get()
方法时才会触发该方法进行初始化 - 若该线程第一次调用了
set()
方法,则该方法将不再会执行 - 若调用
remove()
方法后,再调用get()
方法,则也会触发该方法再次进行初始化
void set(T value)
:为该线程对应的ThreadLocal
设置一个新值- 先获取当前线程的
ThreadLocalMap
,若获取到的是null就创建该map对象并设置值,若不是null就调用map.set(this,value)
覆盖掉之前的值
- 先获取当前线程的
T get()
:获取该线程对应ThreadLocal
的value值,若该ThreadLocal
未初始化则会调用InitialValue()
方法进行初始化- 先获取当前线程的
ThreadLocalMap
然后调用map.getEntry(this)
方法,将ThreadLocal
的引用作为参数,获取map中对应的value
- 先获取当前线程的
vodi remove()
:删除该线程对应ThreadLocal
的值- 先获取当前线程的
ThreadLocalMap
然后调用map.remove(this)
方法,将ThreadLocal
的引用作为参数,删除map中对应的value
- 先获取当前线程的
ThreadLocalMap
ThreadLocalMap
是ThreadLocal
的静态内部类,是以ThreadLocal
为键的一个map集合ThreadLocalMap
解决哈希冲突的方式是线性探测法,而不是拉链法,若发生哈希冲突就向后寻找找空位置填入
ThreadLoacl注意事项
内存泄漏
ThreadLocalMap
中的key继承WeakReference
,是弱引用,若对象只被弱引用关联,那么该对象是可被GC回收的,所以ThreadLocalMap
中的key是不会发生内存泄漏的
ThreadLocalMap
中的value是直接赋值的,是强引用,当线程终止时,该线程对象会被回收该线程内的ThreadLocalMap
也会被回收,弱线程无法终止(比如使用的线程池),那么key(由于key是弱引入用,所以key可能为null)对应的value就不能被回收,就可能出现内存泄漏的问题
ThreadLocal
中的set()
、remove()
、rehash()
方法中都会调用resizt()
方法扫描key为null的对象,将其value也置为null,从而可被CG回收,所以使用完ThreadLocal
后就应该调用remove()
方法来防止内存泄漏
空指针异常
以下情况都会返回null,就有可能报空指针异常
- 若不重写
initialVlaue()
方法直接调用get()
方法 - 或未先调用
set()
方法就调用get()
方法 - 或调用
remove()
方法后就调用get()
方法
共享对象
若ThreadLocal
中存储的本身就是一个共享对象(比如静态对象),那么还是获取该共享对象本身,若该共享对象存在并发安全问题,则取出来的对象一样存在并发问题
其他注意点
- 若可以不使用
ThreadLocal
就可以解决的问题,无需强行使用,比如任务数很少的情况 - 优先使用框架的支持,而不是自己创造,比如Spring中,若可以使用
RequestContextHolder
、DateTimeContextHolder
等对ThreadLocal
的封装,无需自己维护ThreadLocal
,以避免忘记调用remove()
方法,从而调用内存泄漏
Comments NOTHING