线程池-ThreadLocal

2 篇文章 0 订阅
1 篇文章 0 订阅

[toc]

ThreadLocal 两大使用场景及用法

典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random )

每个Thread内有自己的实例副本,不共享
比喻:教材只有一本,一起做笔记有线程安全问题。复印后没问题

  • SimpleDateFormat的进化之路
  1. 只有两个线程分别用自己的SimpleDateFormat

在这里插入图片描述

  1. 多个线程共用一个日期格式化器
    在这里插入图片描述
    同样的日期格式化器创建了许多

在这里插入图片描述

  1. 每个线程共用一个日期格式化器

    面临问题:线程安全问题,所有线程共用同一个日期格式化器

在这里插入图片描述

  • 问题解决
  1. 格式化方式使用 synchronized 同一时刻只有一个线程能调用格式化日期的方法
  2. 更好的解决方案是使用 ThreadLocal

在这里插入图片描述

  • 总结进化之路
  1. 两个线程分别用自己的SimpleDateFormat,这没问题
  2. 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅(应该复用对象),但勉强可以接受
  3. 但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多)
  4. 面临问题:所有的线程都共用同一个simpleDateFormat对象,这是线程不安全的,出现了并发安全问题
  5. 我们可以选择加锁,加锁后结果正常,但是效率低
  6. 在这里更好的解决方案是使用ThreadLocal
  7. lambda表达式
  • 代码示例
  1. 使用线程池方式:使用同一个日期格式化工具
/**
 * @author jichao
 * @version V1.0
 * @description: 多个任务执行日期格式化操作
 * @date 2020/09/16
 */
public class ThreadLocalNormalUse {

    /**
     * 1. 如果每次调用转换方法都创建一个工具类,会增加内存消耗
     * 2. 所有线程共用一份日期格式化工具类,会出现线程安全问题
     */
    public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(()->{
                String time = new ThreadLocalNormalUse().convertDate(finalI);
                System.out.println(time);
            });
        }
        executorService.shutdown();
    }

    /**
     * 日期格式化
     * @param time
     * @return
     */
    private String convertDate(int time) {
        Date date = new Date(time * 1000);
        return simpleDateFormat.format(date);
    }
}
  1. 加锁解决线程安全问题
/**
 * @author jichao
 * @version V1.0
 * @description: 加锁方式
 * @date 2020/09/16
 */
public class ThreadLocalNormalUse2 {

    /**
     * 1. 如果每次调用转换方法都创建一个工具类,会增加内存消耗
     * 2. 所有线程共用一份日期格式化工具类,会出现线程安全问题
     */
    public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("format date");
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10000; i++) {
            int finalI = i;
            executorService.execute(()->{
                String time = new ThreadLocalNormalUse2().convertDate(finalI);
                System.out.println(time);
            });
        }
        executorService.shutdown();

        while (true) {
            if (executorService.isTerminated()) {
                stopWatch.stop();
                System.out.println(stopWatch.prettyPrint());
                break;
            }
        }
    }
    /**
     * 日期格式化
     * @param time
     * @return
     */
    private String convertDate(int time) {
        Date date = new Date(time * 1000);
        String timeRet = null;
        synchronized (ThreadLocalNormalUse2.class) {
            timeRet = simpleDateFormat.format(date);
        }
        return timeRet;
    }
}
  1. 使用 ThreadLocal 方式
/**
 * @author jichao
 * @version V1.0
 * @description: 使用ThreadLocal 方式  利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
 * @date 2020/09/16
 */
public class ThreadLocalNormalUse3 {


    public static void main(String[] args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("format date");
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10000; i++) {
            int finalI = i;
            executorService.execute(()->{
                String time = new ThreadLocalNormalUse3().convertDate(finalI);
                System.out.println(time);
            });
        }
        executorService.shutdown();
        while (true) {
            if (executorService.isTerminated()) {
                stopWatch.stop();
                System.out.println(stopWatch.prettyPrint());
                break;
            }
        }
    }

    /**
     * 日期格式化
     * @param time
     * @return
     */
    private String convertDate(int time) {
        Date date = new Date(time * 1000);
        return SimpleDateFormatSafe.simpleDateFormat2.get().format(date);
    }

}

class SimpleDateFormatSafe{

    public static ThreadLocal<SimpleDateFormat> simpleDateFormat = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    /**
     * lambda 方式
     */
    public static ThreadLocal<SimpleDateFormat> simpleDateFormat2 = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

  • 案例

    当前用户信息需要被线程内所有方法共享,一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),再从service-2()传到service-3(),以此类推,但是这样做会导致代码冗余且不易维护

在这里插入图片描述

  • 实现

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

  • 实现方式1

  1. 在案例基础上可以演进,使用UserMap

  2. 当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但无论用什么,都会对性能有所影响

在这里插入图片描述

  • 实现方式2
  1. 更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可达到保存当前线程对应的用户信息的目的

  2. 其实就是将变量放入的本地线程中,不涉及到资源共享问题,所以解决了线程安全问题

  • 思路
  1. 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID等)

  2. 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的

  3. 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦

  4. 强调的是同一个请求内(同一个线程内)不同方法间的共享

  5. 不需重写initialValue()方法,但是必须手动调用set()方法

  • 代码示例
public class ThreadLocalNormalUse4 {


    public static void main(String[] args) {
        UserSerivce userSerivce = new UserSerivce();
        userSerivce.process();
    }
}

class UserSerivce {
    public void process() {
        User user = new User();
        user.setName("大圣神威");
        UserHoderUtil.userContextHolder.set(user);
        UserSerivce2 userSerivce2 = new UserSerivce2();
        userSerivce2.process();
    }
}

class UserSerivce2 {
    public void process() {
        User user = UserHoderUtil.userContextHolder.get();
        System.out.println("UserSerivce2 ... userName = " + user.getName());
        UserSerivce3 userSerivce3 = new UserSerivce3();
        userSerivce3.process();
    }
}

class UserSerivce3 {
    public void process() {
        User user = UserHoderUtil.userContextHolder.get();
        System.out.println("UserSerivce3 ... userName = " + user.getName());
    }
}

class UserHoderUtil {
    public static ThreadLocal<User> userContextHolder = new ThreadLocal<>();
}

@Data
class User {
    private String name;
}

总结

  • ThreadLocal的两个作用
  1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  2. 在任何方法中都可以轻松获取到该对象
  • 根据共享对象的生成时机不同,选择initialValueset来保存对象

场景一:initialValue

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

场景二:set

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

使用ThreadLocal带来的好处

  • 达到线程安全

  • 不需要加锁,提高执行效率

  • 更高效地利用内存、节省开销:相比于每个任务都新建一个 SimpleDateFormat,显然用 ThreadLocal 可以节省内存和开销

  • 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过 ThreadLocal 拿到,再也不需要每次都传同样的参数。ThreadLocal 使得代码耦合度更低,更优雅

ThreadLocal原理、源码分析

  • 搞清楚 Thread、ThreadLocal 以及 ThreadLocalMap 三者之间的关系
  1. 每个Thread对象中都持有一个ThreadLocalMap 成员变量

  2. 每一个 Thread 中都有一个 ThreadLocalMap,并且里面可以放入多个 ThreadLocal

  • 示例图

在这里插入图片描述

主要方法介绍

  • TinitialValue()∶初始化
  1. 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get 的时候,才会触发

get 方法默认调用的就是初始化方法,如果不重写,默认返回null

  1. 当线程第一次使用 get 方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本 initialValue 方法

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

  3. 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写 initialValue() 方法,以便在后续使用中可以初始化副本对象。

  • void set(T t):为这个线程设置一个新值

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

get 方法是先取出当前线程的 ThreadLocalMap,然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入,取出map中属于本 ThreadLocal 的 value

注意:这个 map 以及 map 中的 key 和 value 都是保存在线程中的,而不是保存在ThreadLocal中

  • void remove():删除对应这个线程的值

  • ThreadLocalMap 类

  1. ThreadLocalMap 类,也就是Thread.threadLocals

  2. ThreadLocalMap 类是每个线程 Thread 类里面的变量,里面最重要的是一个键值对数组 Entry [] table,可以认为是一个map,键值对∶

    键:这个 ThreadLocal
    值:实际需要的成员变量,比如 user 或者 simpleDateFormat 对象

数据结构

  • HashMap

在这里插入图片描述

  • ThreadLocalMap 这里采用的是线性探测法,也就是如果发生冲突,就继续找卞一个空位置,而不是用链表拉链

两种使用场景殊途同归

通过源码分析可以看出,setInitialValue 和直接 set 最后都是利用 map.set() 方法来设置值

也就是说,最后都会对应到 ThreadLocalMap 的一个Entry,只不过是起点和入口不一样

ThreadLocal 注意点

内存泄露

  • 什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收

  • Key 的泄漏:ThreadLocalMap 中的 Entry 继承自 WeakReference,是弱引用

弱引用的特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收

  • Value 的泄漏
  1. ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,同时,每个Entry 都包含了一个对 value 的强引用

  2. 正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被垃圾回收,因为没有任何强引用了,但是,如果线程不终止(比如线程需要保持很久),那么 key 对应的 value 就不能被回收,因为有以下的调用链:

    Thread > ThreadLocalMap > Entry ( key为null ) > Value

    因为 value 和 Thread 之间还存在这个强引用链路,所以导致 value 无法回收,就可能会出现OOM

  3. JDK 已经考虑到了这个问题,所以在 set, remove,rehash 方法中会扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就可以被回收

  4. 但是如果一个 ThreadLocal 不被使用,那么实际上 set,remove,rehash 方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了 value 的内存泄漏

  • 如何避免内存泄露(阿里规约)

    调用 remove 方法,就会删除对应的 Entry 对象,可以避免内存泄漏,所以使用完 ThreadLocal 之后,应该调用 remove 方法

空指针异常

  • 代码示例TODO

    在进行 get 之前,必须先 set,否则可能会报空指针异常?

由于返回类型会存在拆箱装箱的问题,在这个过程中会报空指针异常

共享对象

如果在每个线程中 ThreadLocal.set() 进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,还是有并发访问问题

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

例如:在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal

优先使用框架的支持,而不是自己创造

例如:在 Spring 中,如果可以使用 RequestContextHolder,那么就不需要自己维护 ThreadLocal,因为自己可能会忘记调用 remove() 方法等,造成内存泄漏

实际应用场景——在Spring中的实例分析

  • DateTimeContextHolder类,看到里面用了ThreadLocal

  • RequestContextHolder类

  • 每次HTTP请求都对应一个线程,线程之间相互隔离,这就是 ThreadLocal 的典型应用场景

7.常见面试题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值