文章目录
前言
本文主要介绍ThreadLocal,包括它的底层结构、源码以及注意事项(内存泄漏问题)一、ThreadLocal案例
案例一:使用线程池打印日期信息
public class ThreadLocalNormalUsage03 {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//利用SimpleDateFormat对象,返回日期信息
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage03().date(10 + finalI);
//打印日期信息(每个线程打印的都不一样)
System.out.println(date);
}
});
}
threadPool.shutdown();
}
}
最后的结果可以发现,会出现相同的值,这说明发生了线程安全问题。因为所有线程公用了一个SimpleDateFormat对象,当发生安全问题时,导致拿到的值相同
这时候可以考虑加锁:
synchronized (ThreadLocalNormalUsage04.class) {
s = dateFormat.format(date);
}
然而加锁的效率太低。
这时候就可以考虑使用ThreadLocal,利用ThreadLocal,给每个线程分配自己的dateFormat对象,既保证线程安全,又高效利用内存
public class ThreadLocalNormalUsage05 {
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
//通过threadLocal的get方法获取到SimpleDateFormat对象
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(10 + finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
//初始化方法一,重写initialValue
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"));
}
案例二:全局变量
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service().process();
}
}
class Service {
public void process() {
User user = new User("张三");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户:" + user.name);
UserContextHolder.holder.remove(); //确保不会出现内存泄漏
}
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
通过ThreadLocal保存全局变量,可以避免传参的麻烦
两种场景总结
二、ThreadLocal原理
1.结构
每个线程存在一个ThreadLocalMap,每个Map里面包含多个ThreadLocal,而每个ThreadLocal都是以K-V形式存储的
2.方法
2.1 initialValue()初始化
-
该方法会返回当前线程对应的"初始值",这是一个延迟加载的方法,只有在调用get的时候,才会触发。
-
当线程第一次使用get方法访问变量时,将调用此方法,除非线程先调用了set方法,在这种情况下,不会为线程调用本initialValue方法。
-
通常每个线程最多调用一次此方法,但如果已经调用了remove()后,在调用get(),则可以再次调用此方法。(remove之后,map变为null)
-
如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
//初始化方法一,重写initialValue
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
2.2 get()
- 拿到当前线程对象
- 获取map
- 如果存在,就去获取entry,即K-V
- 拿到entry后如果不为null,就可以拿到value
2.3 set()
public static ThreadLocal<User> holder = new ThreadLocal<>();
holder.set(user);
- 拿到map
- 向map里面存K-V
2.4 remove()
- 删除掉当前的entry
3.ThreadLocalMap
- threadLocalMap类似于是一个HashMap
- 它在发生冲突的时候采用的是线性探测法,就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链
4.内存泄漏
- 当某个对象不再有用,但是占用的内存却不能被回收。
ThreadLocal就存在内存泄漏的问题
在上面的ThreadLocalMap里面,最重要的就是Entry对象,它包含key-value,可以看到整个Entry对象继承WeakReference是弱引用(当某个对象只被弱引用关联,它就可以被回收),然而,value=v属于强引用
- 正常情况下,当线程停止,它里面的value就会被垃圾回收,那么就不存在强引用了
- 但是当线程不终止,那么key就不能被回收,就存在
Thread——>ThreadLocalMap——>Entry(key为null)——>Value
JDK已经考虑到了这样的问题,在set,remove,rehash这些方法中会扫描key为null的Entry,并把value设置为null,这样就可以被回收
但是,若这些方法都没有被调用,线程又不停止,那么就会出现内存泄漏
在使用完ThreadLocal之后(业务逻辑中不需要再使用),主动调用remove()方法,避免内存泄漏。
总结
复习重点:initialize()、get()、内存泄漏