是什么?
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
作用
实现每一个线程都有自己专属的本地变量副本。主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
核心api
private static class House {
//创建线程局部变量,并指定初始值
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
public void saleHouse() {
Integer value = threadLocal.get();
value++;
threadLocal.set(value);
}
}
public static void main(String[] args) {
House house = new House();
new Thread(()->{
try {
for (int i = 0; i < 3; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName() + " " + house.threadLocal.get());
}finally {
house.threadLocal.remove();
}
},"t1").start();
new Thread(()->{
try {
for (int i = 0; i < 2; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName() + " " + house.threadLocal.get());
}finally {
house.threadLocal.remove();
}
},"t2").start();
new Thread(()->{
try {
for (int i = 0; i < 5; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName() + " " + house.threadLocal.get());
}finally {
house.threadLocal.remove();
}
},"t3").start();
}
结果:
t1 3
t2 2
t3 5
- 因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用
- 既然其它 Thread 不可访问,那就不存在多线程间共享的问题
- 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
线程不安全的类SimpleDateFormat
线程不安全示例
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < 30; i++) {
new Thread(()->{
try {
System.out.println(sdf.parse("2020-11-11 11:11:11"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
public class UnSafeDateUtils {
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parseDate(String dateStr) throws ParseException {
return sdf.parse(dateStr);
}
public static void main(String[] args) {
for (int i = 0; i < 30; i++) {
new Thread(()->{
try {
System.out.println(UnSafeDateUtils.parseDate("2020-11-11 11:11:11"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
改进
使用ThreadLocal解决SimpleDateFormat线程不安全问题
public class DateUtils {
public static final ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parseDate(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
public static void remove() {
threadLocal.remove();
}
public static void main(String[] args) {
for (int i = 0; i < 30; i++) {
new Thread(()->{
try {
System.out.println(DateUtils.parseDate("2020-11-11 11:11:11"));
} catch (ParseException e) {
e.printStackTrace();
}finally {
remove();
}
}).start();
}
}
}
使用DateTimeFormatter 代替 SimpleDateFormat
public class DateUtils {
public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String format(LocalDateTime localDateTime) {
return DATE_TIME_FORMAT.format(localDateTime);
}
public static LocalDateTime parse(String dateString) {
return LocalDateTime.parse(dateString,DATE_TIME_FORMAT);
}
}
ThreadLocal中ThreadLocalMap的数据结构和关系?
ThreadLocalMap是以ThreadLocal实例为key,任意对象为value的Entry对象。
public class ThreadLocalTest {
private static final ThreadLocal<String> localVariable1 = new ThreadLocal<>();
private static final ThreadLocal<String> localVariable2 = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
localVariable1.set("t1 set localVariable1");
localVariable2.set("t2 set localVariable2");
//避免线程运行结束
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(()->{
localVariable1.set("t1 set localVariable1");
//避免线程运行结束
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
System.out.println();
}
}
每个Thread都会维护当前类的一个或多个ThreadLocal实例
t1线程有两个Entry对象
t2线程有1个Entry对象
小总结:
每个Thread对象维护着一个ThreadLocalMap的引用。
ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象。
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
ThreadLocal只是一个工具类,具体存放变量的是Thread的threadLocals变量。P336
ThreadLocal内存泄露问题
什么是内存泄漏?
不再会被使用的对象或变量占用的内存不能被回收
为什么源代码用弱引用?
public void function01()
{
ThreadLocal tl = new ThreadLocal<Integer>(); //line1
tl.set(2021); //line2
tl.get(); //line3
}
line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;
line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象。
- 当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
- 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
- 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
- 此后我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。
- 当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
- 当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
- 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
key为null的entry,原理解析
- ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。
- 虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
set、get方法会去检查所有键为null的Entry对象,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。