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());
            }
        }
    }
}

测试结果

  1. 第一行输出,是在主线程的main方法中创建account对象,同时给主线程的ThreadLocal属性和普通属性赋值
  2. 第0次循环的输出,两个线程账户获取自己的account对象的threadLocal的值,输出结果都为null;但是普通属性的输出是有值且相同的
  3. 第1次循环的输出,MyThread类中i == 1 && "甲".equals(getName())时,替换当前线程的threadLocal属性的值为当前线程名且同时替换account的普通属性值。
Account初始化==>main线程账户:初始名,账号1234560次循环==>甲线程账户:null,账号:1234560次循环==>乙线程账户:null,账号:1234561次循环==>甲线程账户:,账号:6543211次循环==>乙线程账户: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内容。