ThreadLocal知识小结

Posted 赵jc

tags:

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

什么是ThreadLocal ?

问题引入:
线程安全的解决方案有两种加锁创建私有变量,但加锁后任务会排队执行,消耗大量时间,每次任务会创建新的私有变量会消耗大量的资源,那么有没有一种更好的方法呢,肯定是有的啦,ThreadLocal是线程的本地变量,每一个线程会创建一个私有变量。
SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。

ThreadLocal 使⽤

ThreadLocal 核⼼⽅法总共有以下三个:

  1. set(T):将内容存储到ThreadLocal。没有 set 操作的 ThreadLocal 容易引起脏数据。
  2. get:从线程中取私有变量。没有 get 操作的 ThreadLocal 对象没有意义。
  3. remove:从线程中移除私有变量。没有 remove 操作容易引起内存泄漏。

代码演示:

  /**
     * ThreadLocal使用
     */
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        //线程执行任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + "存入值" + threadName);
                //在ThreadLocal中设置值
                threadLocal.set(threadName);
                //执行方法,打印线程中设置的值
                print(threadName);
            }
        };
        //创建并启动线程1
        new Thread(runnable, "MyThread-1").start();
        //创建并启动线程2
        new Thread(runnable, "MyThread-2").start();
    }
    /**
     * 打印线程中ThreadLocal值
     */
    private static void print(String threadName) {
        try{
            //得到ThreadLocal中得值
            String result = threadLocal.get();
            //打印结果
            System.out.println(threadName + "取出值" + result);

        }finally {
            //移除ThreadLocal中得值
            threadLocal.remove();
        }
    }

在这里插入图片描述
ThreadLocal有两种初始化方法initialValue和
initialValue ⽅法(线程级别的)需要new一个变量

private static ThreadLocal<Integer> threadLocal =
            new ThreadLocal() {
                @Override
                protected Integer initialValue() {
                    int num = new Random().nextInt(10);
                    System.out.println("执行了 initialValue 生成了:" + num);
                    return num;
                }
            };

    public static void main(String[] args) {
        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1, 1,
                0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1000));

        executor.submit(new Runnable() {
            @Override
            public void run() {
                // get ThreadLocal
                int result = threadLocal.get();
                System.out.println(Thread.currentThread().getName() +
                        " 得到结果1:" + result);
            }
        });

        executor.submit(new Runnable() {
            @Override
            public void run() {
                // get ThreadLocal
                int result = threadLocal.get();
                System.out.println(Thread.currentThread().getName() +
                        " 得到结果2:" + result);
            }
        });

    }

在这里插入图片描述
withInitial ⽅法,静态方法不需要new对象

      // 创建并初始化 ThreadLocal
        private static ThreadLocal<String> threadLocal =
                ThreadLocal.withInitial(new Supplier<String>() {
                    @Override
                    public String get() {
                        System.out.println("执行了 withInitial 方法");
                        return Thread.currentThread().getName() + " Java";
                    }
                });
/**
// 创建并初始化 ThreadLocal lambada表达式写法
    private static ThreadLocal<String> threadLocal =
            ThreadLocal.withInitial(() -> "Java");
*/



        public static void main(String[] args) {

            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    // 获取 ThreadLoal
                    String result = threadLocal.get();
                    System.out.println(Thread.currentThread().getName() +
                            " 获取到的内容:" + result);
                }
            };
            Thread t1 = new Thread(runnable, "线程1");
            t1.start();

            Thread t2 = new Thread(runnable, "线程2");
            t2.start();

        }

注意:如果执行了ThreadLocal的set方法,之后就不会执行initialValue方法了,这是由于ThreadLocal.get()方法的源码的设计了,它是懒加载的方式,initialValue方法在初始化ThreadLocal时并不会立刻执行,而是先调用get()方法之后再执行相应的代码,如果ThreadLocal有值的话直接返回,如果没有的话才会调用initialValue方法进行初始化
在这里插入图片描述

ThreadLocal 的使用场景

  • 解决线程安全的问题
  • 实现线程级别的数据传递(实现一定程度上的解耦)
    传统数据传递方式有两种,调用一个类在return的方式和将参数传递过来,这样会增加代码之间的耦合度
  private static ThreadLocal<User> userThreadLocal
            = new ThreadLocal();

    /**
     * 实体类
     */
    static class User {
        private String name;
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    public static void main(String[] args) {
        Storage1 storage1 = new Storage1();
        Storage2 storage2 = new Storage2();

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 用户登录
                User user = new User();
                user.setName("张三");
                userThreadLocal.set(user);
                // 打印用户的信息
                storage1.printUserName();
                storage2.printUserName();
            }
        });
        thread.start();

    }
    /**
     * 仓储类
     */
    static class Storage1 {
        public void printUserName() {
            User user = userThreadLocal.get();
            System.out.println("用户名" + user.getName());
        }
    }
    /**
     * 类 2
     */
    static class Storage2 {
        public void printUserName() {
            User user = userThreadLocal.get();
            System.out.println("用户名" + user.getName());
        }
    }

ThreadLocal 注意事项

  • 不可继承性
    我们可以使用InheritableThreadLocal来解决不可继承性的问题,但前提两个线程必须是父子线程的关系(或者从属进程的关系),不能实现并列线程之间的数据传输(数据设置和获取)为什么会出现这种情况?因为⽆论是 ThreadLocal 还是 InheritableThreadLocal 本质都是线程本地变量,所以不能跨线程进⾏数据共享也是正常的。
//可以拿到对应的值
    // 创建 ThreadLocal
    private static ThreadLocal threadLocal =
            new InheritableThreadLocal();

    public static void main(String[] args) {
        // 在主线程里面设置值
        threadLocal.set("Java");

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("获取值:" +
                        threadLocal.get());
            }
        });
        t1.start();

    }
//不嫩拿到级别相同线程的值
// 创建 InheritableThreadLocal
    private static ThreadLocal threadLocal =
            new InheritableThreadLocal();

    public static void main(String[] args) throws InterruptedException {


        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 在主线程里面设置值
                threadLocal.set("Java");
                System.out.println("线程1 执行值");
            }
        });
        t1.start();
        // 线程 1 执行完
        t1.join();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2得到取:" + threadLocal.get());
            }
        });
        t2.start();

    }
  • 产生脏读问题
    在使⽤线程池的时候,因为线程池会复⽤线程,那么和线程绑定的静态属性也会被复⽤(如:ThreadLocal),所以如果下⼀个线程不执⾏ set 操作或者上⼀个线程不执⾏ remove 操作,那么下⼀个 线程就可以读取到旧值
 static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                MyThreadLocal myThreadLocal = new MyThreadLocal();
                myThreadLocal.show();
            }
        }, "线程1");
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                MyThreadLocal myThreadLocal = new MyThreadLocal();
                myThreadLocal.show();
            }
        }, "线程2");
        t2.start();

    }

    static class MyThreadLocal {
        private static boolean flag = false;

        public void show() {
            String tname = Thread.currentThread().getName();
            if (!flag) {
                // 第一次执行
                threadLocal.set(tname);
                System.out.println(tname + " 设置了:" + tname);
                flag = true;
            }
            System.out.println(tname + "得到了:" + threadLocal.get());
        }
    }

在这里插入图片描述
脏读的解决方案
避免使用静态变量
使用完之后执行remove()操作

  • 内存溢出(配合线程池使用)
    当一个线程使用完资源之后,没有释放资源,或者说释放资源不及时就是内存溢出。原因:线程池是长周期声明的,并且ThreadLocal的value(1mb)是强引用不会释放资源。

ThreadLocal实现原理

Java引用类型4种类型

  • 强引用Strong Reference: 最为常见,如Object object = newObject(),这样的变量的生命的定义就会产生对该对象的强引用,在Java内存回收时,即使发生OOM,也不会回收该对象
  • 软引用Soft Reference:它的引用力度仅次于"强引用",如果内存够用,那么垃圾回收不会考虑回收此引用,在将要发生OOM时才会回收此引用
  • 弱引用Weak Reference: 不管内存够不够用,下一次回收都会将此引用的对象回收掉
  • 虚引用Phantorn Reference:是一种极弱的引用关系,创建既回收,它可以触发一个垃圾回收的回调

从源码开始分析 set/get ⽅法:
set和get都会先调用getMap(),getMap()会返回当前线程的threadlocals(其实是一个ThreadLocalMap(存储的是所有线程的本地变量)),ThreadLocalMap中有一个Entry[]数组,其中Entry的key是当前ThreadLocal的值(是弱引用),保存的值是value(是强引用),下面根根据图片来理解一下在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
ThreadLocalMap解决hash冲突的方法?
开放寻址法(因为一般数据量比较小)

以上是关于ThreadLocal知识小结的主要内容,如果未能解决你的问题,请参考以下文章

Java面试题必备知识之ThreadLocal

Android 开发也要掌握的Java知识 -ThreadLocal

Android进阶你必须要了解的知识:ThreadLocal

带你整理面试过程中关于ThreadLocal的相关知识

关于ThreadLocal的九个知识点,看完别再说不懂了!

java进阶之路-java中的threadlocal源码实现