对ThreadLocal的深入理解

Posted Putarmor

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对ThreadLocal的深入理解相关的知识,希望对你有一定的参考价值。

1.ThreadLocal初识

讲ThreadLocal之前,我们先回顾一下线程不安全的解决方案:
1.给对象加锁(synchronized加锁或Lock加锁)
2.让线程拥有私有变量,不让多个线程修改一个变量

对于1方式而言,加锁使得线程排队执行,具有性能消耗;对于2方式而言,设置私有变量的方法有一个缺陷,当任务数量比较多时,每一次任务执行都会创建私有变量,这会导致大量内存被占用,浪费空间。

因而Java中有一种比较高效的解决线程安全的方案:ThreadLocal

ThreadLocal是线程级别的私有变量,与任务级别的私有变量是完全不同的!

举例:

创建一个线程池,核心线程数与最大线程数都是10,任务数为1000

Public void fun(){
	Object obj = new Object();
	//...
}

对于任务级别的私有变量而言,执行1000次任务同时也会创建1000次变量;如果是ThreadLocal线程级别私有变量,每个线程拥有一个变量,执行1000次任务过程中,因为10个线程实现了复用,所以只创建了10次变量。

2.ThreadLocal方法

1)基本方法

1.set:将变量设置到线程中
2.get:从线程中取得私有变量
3.remove:从线程中移除私有变量(不移除会导致脏读、幻读)
4.initialValue:初始化
5.withInitial:初始化
ThreadLocal使用:

public class ThreadPool13 {
    // 创建 ThreadLocal
    static ThreadLocal<String> threadLocal =
            new ThreadLocal<>();

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // set threadLocal
                    String tname = Thread.currentThread().getName();
                    threadLocal.set(tname);
                    System.out.println(String.format("线程%s 设置了值:%s",
                            tname, tname));
                    printTName();
                } finally {
                    threadLocal.remove();
                }
            }
        }, "1");
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    String tname = Thread.currentThread().getName();
                    // set ThreadLocal
                    threadLocal.set(tname);
                    System.out.println(String.format("线程%s 设置了值:%s",
                            tname, tname));
                    printTName();
                } finally {
                    threadLocal.remove();
                }
            }
        }, "2");
        t2.start();
    }

    private static void printTName() {
        String tname = Thread.currentThread().getName();
        // 得到存放在 threadLoca 中的值
        String result = threadLocal.get();
        System.out.println(String.format("线程%s 取得:%s",
                tname, result));
    }
}

在这里插入图片描述

2)initialValue()的使用

public class ThreadPool14 {
    static ThreadLocal<String> threadLocal = new ThreadLocal(){
        @Override
        protected Object initialValue() {
            System.out.println("执行了初始化方法");
            return "trq";
        }
    };

    public static void main(String[] args) {
        String result = threadLocal.get();
        System.out.println("读取到的内容是:"+result);
    }
}

注意事项:initialValue返回的数据类型需要和ThreadLocal定义的泛型类型相同。

public class ThreadPool14 {
    static ThreadLocal<String> threadLocal = new ThreadLocal(){
        @Override
        protected Object initialValue() {
            System.out.println("执行了初始化方法");
            return "trq";
        }
    };

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000));
        for(int i = 0; i < 2; i++){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    String result = threadLocal.get();
                    System.out.println("读取到的内容是:"+result);
                }
            });
        }
    }
}

在这里插入图片描述


我们发现initialValue与get可以一起操作,实现正常存取
那么当initialVale与set、get一起操作行不行呢?
答案:不可以。当三个方法都存在时,只会执行set与get,当set后initialValue方法不会被执行。
为什么不会执行呢?
ThreadLocal和线程池一样具有懒加载机制,在创建的时候不能立刻去执行,只是标识其他任务可以调用自己;在创建ThreadLocal并编写初始化方法时没有去执行实例化,其他地方调用ThreadLocal方法后才会去加载。从源码中可以看出,当执行了get方法后,才会去尝试执行initialValue方法;尝试获取ThreadLocal的set方法值,如果获取到了,那么初始化方法永远不被执行。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3)withInitial()使用

public class ThreadPool15 {

    //因为withInitial是静态方法,所以无需new操作
    static ThreadLocal<String> threadLocal =
            ThreadLocal.withInitial(new Supplier<String>() {
                @Override
                public String get() {
                    System.out.println("执行了初始化方法!");
                    return "Java";    //初始化时候返回的值
                }
            });

    public static void main(String[] args) {
        try{
            String result = threadLocal.get();
            System.out.println(result);
        }finally{
            threadLocal.remove();
        }

        String str = threadLocal.get();
        System.out.println(str);
    }
}
!!!remove在任何场景下都可以使用

程序执行结果如下:
在这里插入图片描述
可以看出withInitial完成了ThreadLocal初始化;此外当我们执行remove后,再一次执行get发现依然执行了初始化的结果,原因就是与源码设计存在关系,get方法中,当map为null时,会去执行SetInitial方法。

3.ThreadLocal使用场景

1)解决线程安全问题

需求:实现1000个任务的格式化

当没有ThreadLocal时使用线程池完成任务,会导致线程不安全发生:在这里插入图片描述
在这里插入图片描述
当使用ThreadLocal时,解决了线程安全:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
分析:可以看出,程序运行结果就是我们的预期结果;初始化方法执行了10次,而不是设置任务的1000次,因此可以说,ThreadLocal变量被线程复用,它是线程级别的私有变量而不是任务级别的。

2)实现线程级别的数据传递

/**
 * 使用场景2:实现线程级别的数据传递
 */

public class ThreadPool17 {

    static class ThreadLocalUnit{
        static ThreadLocal<User> threadLocal = new ThreadLocal<>(); //!!只写成静态属性的话只能在当前类中访问
    }

    static class User{
        private String username;

        public String getUsername(){
            return username;
        }

        public void setUsername(String username){
           this.username = username;
        }
    }

    /*
    日志信息
     */
    static class Logger{
        public void addLog(){
            User user = ThreadLocalUnit.threadLocal.get();
            System.out.println("添加日志:"+user.getUsername());


        }
    }

    /*
    订单系统
     */
    static class Order{
        public void getOrder(){
            User user = ThreadLocalUnit.threadLocal.get();
            System.out.println("订单列表:"+user.getUsername());
        }
    }


    public static void main(String[] args) {
        //模拟用户的登录操作
        User user = new User();
        user.setUsername("TRQ");
        ThreadLocalUnit.threadLocal.set(user);

        //调用订单系统
        Order order = new Order();
        order.getOrder();

        //调用日志系统
        Logger logger = new Logger();
        logger.addLog();

    }
}

在这里插入图片描述

4.ThreadLocal缺点

1)父子线程间的不可继承性

在这里插入图片描述在这里插入图片描述
从执行结果可以看出,子线程没有获取到主线程中设置的ThreadLocal变量,这就是不可继承性的体现。

怎么解决不可继承性?

解决方法:将要实例化的ThreadLocal类改为它的子类InheritableThreadLocal,这样就解决不可继承性问题。

static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

上述代码修改后执行结果:
在这里插入图片描述

2)不能数据共享

InheritableThreadLocal不能实现不同子线程之间的数据共享

在这里插入图片描述
在这里插入图片描述
从结果可以看出,线程2没有获取到线程1设置的ThreadLocal变量。

3)脏读问题

一个线程读到了不属于自己的数据,这就出现了脏读现象。

public class ThreadPool20 {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(1, 1, 0,
                        TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        for (int i = 0; i < 2; i++) {
            MyTask t = new MyTask();
            executor.execute(t);
        }
    }

    static class MyTask extends Thread {
        // 标志位,标识是否第一次访问
        static boolean first = true;

        @Override
        public void run() {
            if (first) {
                // 第一次访问,存储用户信息
                threadLocal.set(this.getName() +
                        " session info.");
                first = false;
            }
            // get threadLocal
            String result = threadLocal.get();
            System.out.println(this.getName() + ":" + result);
        }
    }
}

在这里插入图片描述
线程1读到了线程0的信息,导致脏读发生。

>>脏读原因

原因:
线程池中使用ThreadLocal出现脏读是因为线程池会复用线程,复用线程时就会复用线程的静态属性,从而导致某些方法不能执行;对于线程而言,使用ThreadLocal不会出现脏读,因为每个线程使用的是自己的私有变量和ThreadLocal。

>>解决脏读办法

1.避免使用静态属性(线程池中静态属性会复用)
2.使用ThreadLocal的remove方法(坚持使用静态属性的情况下解决方法)

4)内存溢出问题(较常出现)

内存溢出:当一个线程执行完了之后,不释放所占用的内存,或者释放内存不及时的情况,称之为"内存溢出"。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
可以看出,使用ThreadLocal发生了内存溢出!(具体原因看后续)

**内存溢出原因:
表层:线程池是长生命周期,而线程是执行完任务就结束了(线程相关资源会释放掉);

5.ThreadLocalMap

1)分析内存溢出(OOM)

2)ThreadLocalMap处理冲突方式

①面试题:HashMap处理冲突的方式与ThreadLocalMap处理冲突的方式有什么区别?

HashMap使用数组加链表的方式解决哈希冲突;ThreadLocalMap使用开放寻址法解决ThreadLocalMap冲突。

为什么二者解决冲突方式要这么设计?

当数据量比较少的情况下,开放寻址法的性能更好,对于ThreadLocal来说,它是存储在线程中的,一般而言线程不会很多,所以使用开放寻址法解决ThreadLocal冲突更好;而HashMap里面一般存储的数据比较多,如果采用开放寻址法效率低下,因此使用链表法是最好的选择。

在这里插入图片描述

强引用:最为常见,如Object() object = new Object(),这样的变量声明和定义就会产生该对象的强引用。只要对象有强引用指向,Java内存回收时,即使内存耗尽,也不会回收该对象

软引用:引用力度小于强引用,用在非必须对象的场景,在即将内存溢出之前,垃圾回收器会把软引用指向的对象加入回收范围,以获得更多的内存空间。

弱引用:引用力度比之前两个都小,也是用来描述非必需对象的,如果弱引用指向的对象只存在弱引用,则对象会在下一次GC时被回收。由于GC时间不确定,弱引用指向对象何时被回收也不不确定。

虚引用:极弱的引用变量,当定义完成后,就无法通过引用获取指定向的对象。(可以说是创建即死亡)

对于Entry数组中的对象而言,其key设置为弱引用,value设置为强引用;

①面试题:那么为什么要把Threadlocal中的key设置为弱引用?
为了最大程度避免OOM(内存溢出)

为什么会发生OOM现象呢?

ThreadLocal是长生命周期,线程池中创建了线程就不销毁,线程拥有ThreadLocal,而ThreadLocal中含有静态内部类ThreadLocalMap,ThreadLocalMap里面含有Entry类型数组,数组中的对象为key-value形式,value为强引用,不管是否发生OOM,强引用都不会被回收掉,因此会出现内存溢出问题。

解决ThreadLocal内存溢出:使用remove方法

以上是关于对ThreadLocal的深入理解的主要内容,如果未能解决你的问题,请参考以下文章

深入剖析ThreadLocal

深入剖析ThreadLocal

深入理解ThreadLocal

深入剖析ThreadLocal

Java并发--深入剖析ThreadLocal

深入理解threadlocal