19-ThreadLoacl

nobility 发布于 2021-06-05 2395 次阅读


ThreadLoacl

应用场景

  • 线程单例:每个线程需要独享单个对象,通常是工具类,比如SimpleDateFormatRandom
  • 线程独享参数提纯:每个线程内需要保存全局变量,让不同方法直接使用,避免传参的麻烦

作用与好处:让需要使用到的对象在线程之间隔离,无需加锁达到线程安全的目的;且在方法中轻松获取,避免传参的麻烦,达到解耦的目的;从而提高了程序的执行效率;在某种意义上节省了内存开销,比如线程不安全的工具类

线程单例

public class Main {

  public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    Set<String> set = new HashSet<>();
    for (int i = 0; i < 1000; i++) {
      int finalI = i;
      executorService.execute(() -> {
        set.add(formatData(finalI * 1000));
      });
    }
    executorService.shutdown();  //任务执行完后关闭线程池
    if (executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MINUTES)) {
      System.out.println(set.size());  //线程池任务都执行结束后查看set中的元素个数
    }
  }

  public static String formatData(long timestamp) {
    Date date = new Date(timestamp);

    //return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
    //工具类对象只需要一个,每次new对象导致浪费内存和GC

    //return simpleDateFormat.format(date);
    //公用一个工具类对象,但是由于该工具类是线程不安全的,所以会有重复的值

    //synchronized (Main.class) {
    //return simpleDateFormat.format(date);
    //使用同步方式可解决共用一个工具类对象的线程不安全问题,但是同步机制需要线程排队效率较低
    //}

    return simpleDateFormatThreadLocal.get().format(date);
    //使用ThreadLocal工具类,既保证线程安全又保证效率
  }

  static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

  static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<>() {  //使用withInitial()静态方法即使用lambda表达式
    @Override
    protected SimpleDateFormat initialValue() {  //设置初始值
      return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    }
  };
}

线程独享参数提纯

public class Main {
  public static void main(String[] args) {
    //new Service1().process("张三");  //层层参数传递,耦合度高
    new Service1().processThreadLocal("张三");
  }
}

class UserNameContextHolder {
  public static ThreadLocal<String> holder = new ThreadLocal<>();  //由于不知道何时进行初始化值,所以需要使用set()方法设置
  //若使用map集合,会有线程安全问题
  //若使用线程安全的map集合,会有性能影响
}

class Service1 {
  public void process(String userName) {
    System.out.println("Service1设置用户名:" + userName);
    new Service2().process(userName);
  }

  public void processThreadLocal(String userName) {
    UserNameContextHolder.holder.set(userName);
    System.out.println("Service1设置用户名:" + userName);
    new Service2().processThreadLocal();  //进入下一个业务
  }
}

class Service2 {
  public void process(String userName) {
    System.out.println("Service2拿到用户名:" + userName);
    new Service3().processThreadLocal();  //进入下一个业务
  }

  public void processThreadLocal() {
    String userName = UserNameContextHolder.holder.get();
    System.out.println("Service2拿到用户名:" + userName);
    new Service3().processThreadLocal();  //进入下一个业务
  }
}

class Service3 {
  public void process(String userName) {
    System.out.println("Service2拿到用户名:" + userName);
    new Service3().processThreadLocal();  //进入下一个业务
  }

  public void processThreadLocal() {
    String userName = UserNameContextHolder.holder.get();
    System.out.println("Service2拿到用户名:" + userName);
  }
}

ThreadLoacl原理

在每个Thread类中都有一个ThreadLocalMap的成员变量,该属性存储着该线程中若干个ThreadLocal对象

ThreadLocal

  • T initialVlaue():该方法会返回当前线程对应ThreadLocal初始值
    • 若不重写该方法,该方法默认返回null
    • 是一个懒加载的方法,只有在第一次调用的是get()方法时才会触发该方法进行初始化
    • 若该线程第一次调用了set()方法,则该方法将不再会执行
    • 若调用remove()方法后,再调用get()方法,则也会触发该方法再次进行初始化
  • void set(T value):为该线程对应的ThreadLocal设置一个新值
    • 先获取当前线程的ThreadLocalMap,若获取到的是null就创建该map对象并设置值,若不是null就调用map.set(this,value)覆盖掉之前的值
  • T get():获取该线程对应ThreadLocal的value值,若该ThreadLocal未初始化则会调用InitialValue()方法进行初始化
    • 先获取当前线程的ThreadLocalMap然后调用map.getEntry(this)方法,将ThreadLocal的引用作为参数,获取map中对应的value
  • vodi remove():删除该线程对应ThreadLocal的值
    • 先获取当前线程的ThreadLocalMap然后调用map.remove(this)方法,将ThreadLocal的引用作为参数,删除map中对应的value

ThreadLocalMap

  • ThreadLocalMapThreadLocal的静态内部类,是以ThreadLocal为键的一个map集合
  • ThreadLocalMap解决哈希冲突的方式是线性探测法,而不是拉链法,若发生哈希冲突就向后寻找找空位置填入

ThreadLoacl注意事项

内存泄漏

ThreadLocalMap中的key继承WeakReference,是弱引用,若对象只被弱引用关联,那么该对象是可被GC回收的,所以ThreadLocalMap中的key是不会发生内存泄漏的

ThreadLocalMap中的value是直接赋值的,是强引用,当线程终止时,该线程对象会被回收该线程内的ThreadLocalMap也会被回收,弱线程无法终止(比如使用的线程池),那么key(由于key是弱引入用,所以key可能为null)对应的value就不能被回收,就可能出现内存泄漏的问题

ThreadLocal中的set()remove()rehash()方法中都会调用resizt()方法扫描key为null的对象,将其value也置为null,从而可被CG回收,所以使用完ThreadLocal后就应该调用remove()方法来防止内存泄漏

空指针异常

以下情况都会返回null,就有可能报空指针异常

  • 若不重写initialVlaue()方法直接调用get()方法
  • 或未先调用set()方法就调用get()方法
  • 或调用remove()方法后就调用get()方法

共享对象

ThreadLocal中存储的本身就是一个共享对象(比如静态对象),那么还是获取该共享对象本身,若该共享对象存在并发安全问题,则取出来的对象一样存在并发问题

其他注意点

  • 若可以不使用ThreadLocal就可以解决的问题,无需强行使用,比如任务数很少的情况
  • 优先使用框架的支持,而不是自己创造,比如Spring中,若可以使用RequestContextHolderDateTimeContextHolder等对ThreadLocal的封装,无需自己维护ThreadLocal,以避免忘记调用remove()方法,从而调用内存泄漏
此作者没有提供个人介绍
最后更新于 2021-06-05