Java-ThreadLocal
Java-ThreadLocal
ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。
每一个线程都有自己的ThreadLocal变量副本
创建如下的测试类,其中有两个静态内部类,Account用于展示ThreadLocal和普通属性在多线程下的值的区别;MyThread用来验证ThreadLocal是线程独享的,普通变量非线程独有。
测试类
public class ThreadLocalTest {
public static void main(String[] args) {
// Account类中有ThreadLocal类型的属性
Account account = new Account("初始名", "123456");
// 启动两个共享同一个account对象的线程
new MyThread(account, "甲").start();
new MyThread(account, "乙").start();
}
static class Account {
/**
* 定义一个ThreadLocal类型的变量,每一个线程都会保留该变量的一个副本
*/
private ThreadLocal<String> threadLocalField = new ThreadLocal<>();
private String num;
/**
* 将str字符串更新为属性ThreadLocal的值
*
* @param str 用于替换ThreadLocal属性
* @param num 用于替换普通属性
*/
public Account(String str, String num) {
//对ThreadLocal的静态内部类进行设置
this.threadLocalField.set(str);
// 下面代码用于访问当前线程的name副本的值
// 输出主线程中的threadLocal变量值
this.num = num;
System.out.println("Account初始化==>"+Thread.currentThread().getName() + "线程账户:" + this.threadLocalField.get() + ",账号" + this.num);
}
public String getThreadLocalFieldContent() {
return threadLocalField.get();
}
public void setThreadLocalFieldContent(String str) {
this.threadLocalField.set(str);
}
public String getNum() {
return num;
}
public void setNum(String num) {
this.num = num;
}
}
static class MyThread extends Thread {
/**
* Account中有ThreadLocal属性,此处每启动一个线程,account中的ThreadLocal属性都是不同的
*/
private Account account;
public MyThread(Account account, String threadName) {
super(threadName);
this.account = account;
}
@Override
public void run() {
//循环2次
for (int i = 0; i < 2; i++) {
//当i == 1且线程名为甲时,替换线程中account对象的threadLocalField属性值;且同时替换掉account对象的普通属性值;
if (i == 1 && "甲".equals(getName())) {
// 甲线程才会去替换account
account.setThreadLocalFieldContent(getName());
account.setNum("654321");
}
//输出当前循环次数的线程名及account的两个属性值内容
System.out.println("第" + i + "次循环==>" + Thread.currentThread().getName() + "线程账户:" + account.getThreadLocalFieldContent() + ",账号:" + account.getNum());
}
}
}
}
测试结果
- 第一行输出,是在主线程的main方法中创建account对象,同时给主线程的ThreadLocal属性和普通属性赋值
- 第0次循环的输出,两个线程账户获取自己的account对象的threadLocal的值,输出结果都为null;但是普通属性的输出是有值且相同的
- 第1次循环的输出,MyThread类中
i == 1 && "甲".equals(getName())
时,替换当前线程的threadLocal属性的值为当前线程名且同时替换account的普通属性值。
Account初始化==>main线程账户:初始名,账号123456
第0次循环==>甲线程账户:null,账号:123456
第0次循环==>乙线程账户:null,账号:123456
第1次循环==>甲线程账户:甲,账号:654321
第1次循环==>乙线程账户:null,账号:654321
ThreadLocal是弱引用
Java中Thread类中有ThreadLocalMap类型的属性threadLocals,它是ThreadLocal的静态内部类,ThreadLocalMap类中有Entry类型的数组table,Entry是ThreadLocalMap的静态内部类,Entry类中有Object类型的属性value,它是ThreadLocal存放的值;Entry继承了WeakReference,并在构造方法中将当前ThreadLocal标记为弱引用。即一个Thread中的线程局部变量(ThreadLocal)存放在ThreadLocalMap的Entry类型数组table中,并且threadLocal本身被标记为弱引用。
Thread与ThreadLocal的关系如下:
GC后Entry的key是否为null
因为ThreadLocal的key是弱引用,在ThreadLocal.get()的时候,发生GC之后,key是否为null?
Java中有四种引用类型:
- 强引用:new出来的对象并且有指向引用就是强引用类型,只要强引用存在,垃圾回收器永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
- 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
- 虚引用:虚引用是最弱的引用,在Java中使用PhantomReference进行定义,虚引用中唯一的作用就是用队列接收对象即将死亡的通知
我们定义如下的测试类与方法:
public class ThreadLocalDemo {
public static void main(String[] args) throws Exception{
Thread iAmBackThread = new Thread(() -> test("i am back", false));
iAmBackThread.start();
iAmBackThread.join();
System.out.println("-----gc后----");
Thread iAmFineThread = new Thread(() -> test("ok i am fine", true));
iAmFineThread.start();
iAmFineThread.join();
}
private static void test(String s,boolean isGc){
try {
// 创建ThreadLocal
ThreadLocal<Object> objectThreadLocal = new ThreadLocal<>();
objectThreadLocal.set(s);
if (isGc){
System.gc();
}
Thread thread = Thread.currentThread();
Class<? extends Thread> threadClass = thread.getClass();
// Thread类中的ThreadLocal.ThreadLocalMap类型的属性threadLocals
Field threadLocals = threadClass.getDeclaredField("threadLocals");
if (!threadLocals.isAccessible()){
threadLocals.setAccessible(true);
}
Object threadLocalMap = threadLocals.get(thread);
Class<?> tlmClz = threadLocalMap.getClass();
// ThreadLocalMap类中的Entry类型数组属性table
Field tableField = tlmClz.getDeclaredField("table");
if (!tableField.isAccessible()){
tableField.setAccessible(true);
}
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (Objects.nonNull(o)){
Class<?> entryClass = o.getClass();
// Entry类中的Object类型属性value
Field valueField = entryClass.getDeclaredField("value");
if (!valueField.isAccessible()){
valueField.setAccessible(true);
}
// Entry类继承的WeakReference的父类Reference中的Object类型属性value
Field referentField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
if (!referentField.isAccessible()){
referentField.setAccessible(true);
}
System.out.println(String.format("弱引用key:%s, 值:%s",referentField.get(o),valueField.get(o)));
}
}
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
测试的输出结果为如下,发现gc之后,我们new的ThreadLocal类型对象引用未被清除。
弱引用key:java.lang.ThreadLocal@6185f869, 值:i am back
弱引用key:java.lang.ThreadLocal@6014e646, 值:java.lang.ref.SoftReference@1b3d05aa
-----gc后----
弱引用key:java.lang.ThreadLocal@4dec0182, 值:ok i am fine
我们修改上面自定义线程类中创建ThreadLocal变量的方法为如下:
// 创建ThreadLocal
// ThreadLocal<Object> objectThreadLocal = new ThreadLocal<>();
// objectThreadLocal.set(s);
new ThreadLocal<>().set(s);
测试的输出结果如下,发现gc之后,我们new的ThreadLocal类型对象引用被清除。
弱引用key:java.lang.ThreadLocal@50c033d6, 值:i am back
弱引用key:java.lang.ThreadLocal@2f3c68d2, 值:java.lang.ref.SoftReference@5c312dc3
-----gc后----
弱引用key:null, 值:ok i am fine
总结:当系统发生gc之后,我们线程中定义的ThreadLocal是否被回收,要看它是否为强引用;强引用则不会被回收,是弱引用则被回收。当ThreadLocal被回收之后,ThreadLocal对应的value并未被回收,这就会导致内存泄漏。为了避免由于弱引用导致的内存泄露,我们要注意创建的ThreadLocal对象为强引用,若需要回收则使用ThreadLocal中的remove方法来清除value内容。