Day290&291.ThreadLocal -Juc

Posted 阿昌喜欢吃黄桃

tags:

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

ThreadLocal

一、使用场景

ThreadLocal当前线程本地可访问的一个副本

image-20210606174706252


1、场景1:每个线程需要一个独享的对象

  • 每个Thread类内有自己的实例副本,不共享

  • 比喻

教材只有一本,一起做笔记有线程安全问题。复印后就没有问题了


  • 案例0: 2个线程分别用直接的SimpleDateFormat

image-20210606182632070

public class ThreadLocalNormalUsage00 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocalNormalUsage00 t = new ThreadLocalNormalUsage00();
                System.out.println(t.date(10));;
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocalNormalUsage00 t = new ThreadLocalNormalUsage00();
                System.out.println(t.date(1007));;
            }
        }).start();
    }


    public String date(int seconds){
        //参数的单位为毫秒
        Date date = new Date(1000 * seconds);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return format.format(date);
    }
}

image-20210606182644679

  • 案例1: 10个线程分别用直接的SimpleDateFormat
public class ThreadLocalNormalUsage01 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 30; i++) {
            int num = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    ThreadLocalNormalUsage01 t = new ThreadLocalNormalUsage01();
                    System.out.println(t.date(num));
                    ;
                }
            }).start();
            Thread.sleep(100);
        }
    }
    
    public String date(int seconds) {
        //参数的单位为毫秒
        Date date = new Date(1000 * seconds);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return format.format(date);
    }
}

image-20210606183354726


  • 案例2: 1000个任务,用线程池打印时间
public class ThreadLocalNormalUsage02 {
    public static ExecutorService pool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int num = i;
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    ThreadLocalNormalUsage02 t = new ThreadLocalNormalUsage02();
                    System.out.println(t.date(num));
                }
            });
        }
        pool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位为毫秒
        Date date = new Date(1000 * seconds);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return format.format(date);
    }
}

这里出现了一个可优化的点

image-20210606183910908

每一个线程都要创建一个SimpleDateFormat对象,然后使用结束后再销毁SimpleDateFormat对象,那这里就创建了1000个SimpleDateFormat对象,但是他们用的功能是相同的。我们能不能直接让他们共用一个SimpleDateFormat对象,这样子就减少对象的创建和销毁


案例3: 优化:1000个任务,用线程池打印时间

那这里就使用静态成员类对象,去让他使用一个 SimpleDateFormat对象,避免重复的创建和销毁image-20210606184208001

发现打印的结果出现了线程安全问题!!!,出现了一些打印一样的时间

image-20210606184327622


↓↓每个线程去获取SimpleDateFormat对象并可能同时调用format()方法,又因为没有锁,就会导致获取SimpleDateFormat对象可能出现重复

image-20210606184612772


案例3: 加锁来解决线程安全问题

能使用volatile来解决问题吗???

原因是不符合volatile的使用场景,使用场景有两种,1:直接赋值场景 ,2:触发器场景

public class ThreadLocalNormalUsage04 {
    public static ExecutorService pool = Executors.newFixedThreadPool(10);
    public static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int num = i;
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    ThreadLocalNormalUsage04 t = new ThreadLocalNormalUsage04();
                    System.out.println(t.date(num));
                }
            });
        }
        pool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位为毫秒
        Date date = new Date(1000 * seconds);

        String result = null;
        synchronized (ThreadLocalNormalUsage04.class) {
            result = ThreadLocalNormalUsage04.format.format(date);
        }
        return result;
    }
}

以上的情况是解决了线程安全问题,当时在高并发的情况下,这里就会出现线程排队的问题,性能就不会很好,那还有更好的解决方案吗????


  • 案例4使用ThreadlLocal
public class ThreadLocalNormalUsage05 {
    public static ExecutorService pool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int num = i;
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    ThreadLocalNormalUsage05 t = new ThreadLocalNormalUsage05();
                    System.out.println(t.date(num));
                }
            });
        }
        pool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位为毫秒
        Date date = new Date(1000 * seconds);
//        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

        //获取ThreadLocal中的SimpleDateFormat对象
        SimpleDateFormat format = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return format.format(date);
    }
}

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        //初始化
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

//lambta写法
class ThreadSafeFormatter2 {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.
            withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

2、场景2:每个线程内需要保存全局变量

  • 一个比较繁琐的解决方案

把user作为一个参数层层在方法中传递,但是这样会导致代码冗余且不易维护

image-20210606192009768


现在可以每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦


  • 最佳解决方案: 使用ThreadLocal
/******
 @author 阿昌
 @create 2021-06-06 19:35
 *******
 *      演示ThreadLocal用法2: 避免传递参数的麻烦
 */
public class ThreadLocalNormalUsage06 {
    //主函数
    public static void main(String[] args) {
        Service1 service1 = new Service1();
        service1.process();
    }
}

//模拟业务类1,设置用户信息
class Service1{
    public void process(){
        User user = new User("阿昌");
        //给ThreadLocal设置用户信息
        UserHolder.threadLocal.set(user);
        new Service2().process();
    }
}

//模拟业务类2,获取用户信息
class Service2{
    public void process(){
        User user = UserHolder.threadLocal.get();
        System.out.println("Service2,拿到用户名:"+user.name);
        new Service3().process();
    }
}

//模拟业务类3,获取用户信息
class Service3{
    public void process(){
        User user = UserHolder.threadLocal.get();
        System.out.println("Service3,拿到用户名:"+user.name);
    }
}

//ThreadLocal持有类
class UserHolder{
    public static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}

//实体类
class User{
    String name;
    public User(String name){
        this.name = name;
    }
}

image-20210606202208097

image-20210606202257940


二、ThreadLocal的作用

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)

格子比作线程

image-20210606204537144


  • 在任何方法中都可以轻松获取到该对象

三、如何选择initValue & set 来保存对象

1、场景1:initValue

初始化时机是由我们控制的

  • 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制

2、场景2:set

初始化时机是不由我们控制的

  • 如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续的使用

四、ThreadLocal的好处

  • 线程安全
  • 不需要加锁,提高执行效率
    • 更高效的利用内存、节省开销

相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销

  • 免去传参的繁琐:

无论是场景一的工具类,还是场景二的用户名,都可以在任务地方直接通过ThreadLocal拿到,再也不需要在方法的形参中再定义传入相同的参数。ThreadLocal使的代码耦合度更低,更优雅。


五、ThreadLocal原理

讲ThreadLocal原理前,需要先搞清楚 【Thread】、【ThreadLocal】、【ThreadLocalMap】之间的关系

image-20210607213230046

  • 每一个Thread类,都对应一个ThreadLocalMap

image-20210607203902299

  • 一个ThreadLocalMap可以存储很多的ThreadLocal(KV键值对的形式)

image-20210607205018615

  • 一个线程可能拥有多个ThreadLocal对象

image-20210607204141285

  • ThreadLocal类里面有一个静态类ThreadLocalMap

他的成员变量为Entry,KV键值对存在

image-20210607204747997


六、ThreadLocal的重要方法

1、initValue():初始化

  • 该方法没有默认实现,且会返回当前线程对应的"初始值",这个是一个延迟加载的方法,只有在调用get的时候,才会触发

让我们看看get()方法是不是调用了initVaule()

image-20210607205447744

image-20210607205305232

image-20210607205329154

image-20210607205405887


  • 当线程第一次使用get()方法访问变量时,将调用initValue方法,除非线程先在此之前调用了set()方法。在这种情况下,不会为线程调用本initVaule()方法

    • 如果使用set设置值

      image-20210607210319922


  • 通常,每个线程最多调用一次initValue()方法即可。但如果已经调用了remove()后,再调用get(),则可以再次调用initValue()。

  • 如果不重写initValue()方法,那就会返回null。一般使用匿名内部类的方法来重写initValue()方法,以便在后续使用汇总可以初始化副本对象

image-20210607210754884


2、set():

为这个线程设置一个新值

image-20210607212353239

  • 这个map以及map中的key和value都是保存在线程中对应的ThreadLocalMap中的的,而不是保存在单个ThreadLocal中

3、get():

得到这个线程对象的value。如果是首次调用get(),则会调用initialize来得到这个值

  • get是先获取到这个线程中的ThreadLocalMap对象,并用this(也就是这个ThreadLocal)作为Key参数传入,获取到这个key对应的Value值

image-20210607211533336

image-20210607211843615


4、remove()

删除对应这个线程的值

image-20210607213046757

不是删除整个ThreadLocalMap对象,而是根据this(也就是当前ThreadLocal对象)来删除对应的threadLocal对象

image-20210607213349394


七、ThreadLocalMap类

  • ThreadLocalMap类,也就是Thread.threadLocals

image-20210607213835423

image-20210607213906107

image-20210607213927252


八、ThreadLocal使用注意点

1、*内存泄露问题

  • 内存泄露

某个对象不再有用,但是占用的内存却不能被回收

  • Key弱引用

image-20210607214700371

image-20210607214810441

  • Value泄露

image-20210607214917309

正常情况下,线程运行终止后,后保存在ThreadLocal里的value会被垃圾回收机制回收,因为没有任何强引用了

如果,使用的是线程池的情况,那么这个线程就不会被终止,而是一直被使用,就会出现大量value没被回收的可能性

调用链

因为线程池的情况,Thread不被线程池终止,一直保存运行,所以他下面的的ThreadLocalMap、Enrty等…都不会被回收,就算key为null,因为key是弱引用,value是强引用,那么如果value不是null,但是key是null,那就会出现key已经被GC回收了,而value还没被回收的情况,出现了内存的泄露问题—>导致OOM

image-20210607215215113


JDK考虑到这个问题,所以当key为null时,就将value也置为null

但是这要求,我们调用rehash、remove、set等方法才会去进行if判断设置值

image-20210607215622363

但是一般情况下,当我们设置了这个threadlocal值后就不会在去调用set/remove/rehash等方法进行设置了,一般只会去get获取这个值


2、如何避免内存泄露问题?

每次当使用完threadlocal后就调用remove方法去删除这个对应的entry对象,可以去避免内存泄露问题

所以,当我们使用完ThreadLocal之后,就去调用remove()方法去删除对应的entry对象

image-20210607220035813


3、*空指针异常问题

  • 代码演示
public class ThreadLocalNPE {

   ThreadLocal<Long> tl =  new ThreadLocal();

   public void set(){
       tl.set(Thread.currentThread().getId());
   }
   public long get(){
       return tl.get();
   }

    public static void main(String[] args) {
        ThreadLocalNPE item = new ThreadLocalNPE();
        System.out.println(item.get());

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                item.set();
                System.out.println(item.get());
            }
        });
        thread.start();
    }
}

image-20210607221216062

这里的,get()方法出现了NPE异常,那为什么呢?

image-20210607221318586

因为 initValue 返回的值是 T,那么上面的get方法返回的是long基本类型,也就是说他会先返回Long,然后自动拆箱,然后再返回long基本类型;这里就出现了错误,因为返回的是null了,那么再对null进行拆箱返回基本类型,就会出现空指针这异常!!!

通过修改get()方法的返回值 ,从long —> Long,就可以解决问题

image-20210607221512580

当我们第一次去调用threadlocal对象的get()方法, 当然他返回的是null值,但是我们操作返回不当,上面的例子有出现隐式的拆箱环节,那么就可能出现null对象再进行拆箱,出现了NPE异常


4、*共享对象

image-20210607221804522

image-20210607221944771


5、如果可以不使用ThreadLocal解决问题,那么就不要强行使用


九、Spring中使用ThreadLocal的场景

image-20210607222659047


  • DateTimeContextHolder

保存一系列的时间上下文

image-20210607222311168


  • RequestContextholder

保存一系列的请求头,请求参数等…

image-20210607222404075


以上是关于Day290&291.ThreadLocal -Juc的主要内容,如果未能解决你的问题,请参考以下文章

《安富莱嵌入式周报》第290期:开源静电便携测试仪,开源音频功放,CAN高波特率设计,超级铁电产品,小米Vela系统,65W USB PD充电器参考设计

计划2017年5月计划

Windows 11 22H2 22621.290 和 22622.290 推送

290. Word Pattern

Leetcode 290. Word Pattern

LeetCode_290. Word Pattern