Java中ThreadLocal的用法及原理详解

 更新时间:2023年09月26日 09:19:33   作者:你好世界wxx  
这篇文章主要介绍了Java中ThreadLocal的用法及原理详解,在并发编程中,如果一个类变量被多个线程操作,会造成线程安全问题,使用ThreadLocal可以让每个线程拥有线程内部的变量,防止多个线程操作一个类变量造成的线程安全问题,需要的朋友可以参考下

1 ThreadLocal简介

ThreadLocal中文是:线程局部变量。

  • 为什么需要ThreadLocal呢?这是因为在并发编程中,如果一个类变量被多个线程操作,会造成线程安全问题。例如多个线程使用同一个 SimpleDateFormat 对象。使用ThreadLocal可以让每个线程拥有线程内部的变量,防止多个线程操作一个类变量造成的线程安全问题。
  • 那是不是可以让多线程中的每个任务都创建一个要用的对象呢?这样做可以避免线程安全问题,但是会造成资源的浪费。例如我们要新建1000个格式化打印时间的任务,每个任务中新建一个 SimpleDateFormat 的对象:
    • 我们可以开辟1000个线程分别执行上述任务,但这种做法太耗费资源了,不可取;
    • 我们可以使用线程池,例如线程池中有10个线程,然后将这1000个任务放到线程池中执行,这样可以实现打印时间的目的,没有线程安全问题,但是新建1000个 SimpleDateFormat 对象太浪费了。
    • 最好的做法是每个线程中创建一个 SimpleDateFormat 对象,这样一共只需要创建10个该对象,即保证了线程安全,又节省了资源。

2 ThreadLocal用法

  • 用法一:每个线程需要一个独享的对象。
  • 用法二:每个线程内需要保存全局变量。

2.1 用法一:线程独享对象

请创建1000个格式化打印时间的任务并执行。

做法:使用线程池,线程池中开辟10个线程,用这10个线程执行这1000个任务,为了防止出现线程安全问题,使用 ThreadLocal 保证每个线程独享一个 SimpleDateFormat 对象,代码如下:

/**
 * 典型场景1:每个线程需要一个独享的对象
 * 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用了内存
 */
public class Main1 {
    public static ExecutorService tp = Executors.newFixedThreadPool(10);
    public String date(int seconds) {
        SimpleDateFormat df = TSF.df.get();  // 获取当前线程拥有的 SimpleDateFormat 对象
        return df.format(new Date(1000 * seconds));
    }
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            tp.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new Main1().date(finalI);
                    System.out.println(date);
                }
            });
        }
        tp.shutdown();
    }
}
class TSF {  // ThreadSafeFormatter
    // 本类中定义的类变量都是线程内部的,可以定义多个
    // 每个类变量的用法都是类似的,即:TSF.类变量名.get()    根据类变量名可以知道返回哪个对象
    // 底层map中存在键值对:(UTSF.df, 该函数的返回值)
    public static ThreadLocal<SimpleDateFormat> df = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

结果会打印出1000个不同的时间。

2.2 用法二:线程全局变量

每个线程都会牵涉到三个服务类:Service1、Service2、Service3,这三个类中都会使用到同一个对象。同一个进程内部这是一个对象,不同进程之间对象不同,请实现该需求。

  • 一种简单的做法是:我们可以在相应的函数中进行参数传递但是这样会导致代码冗余且不易维护,不可取。
  • 做法应该是:使用ThreadLocal保存属于每个线程的对象,然后通过ThreadLocal的 get 方法获取属于本线程的对象。
/**
 * 每个线程内需要保存全局变量
 * 同一个线程内该全局信息相同,不同线程间该全局信息不同
 * 如下两个线程,线程1保存全局用户"wxx",线程2保存全局用户"she"
 */
public class Main2 {
    public static void main(String[] args) throws Exception {
        new Thread(() -> new Service1().process("wxx")).start();
        Thread.sleep(100);
        new Thread(() -> new Service1().process("she")).start();
    }
}
class Service1 {  // Service1 调用 Service2
    public void process(String name) {
        User user = new User(name);
        UserContextHolder.holder.set(user);  // 底层map中存在键值对:(UserContextHolder.holder, user)
        System.out.println("Service1:" + user.name);
        new Service2().process();
    }
}
class Service2 {  // Service2 调用 Service3
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2:" + user.name);
        new Service3().process();
    }
}
class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3:" + user.name);
    }
}
class UserContextHolder {  // 本类中定义的类变量都是线程内部的,可以定义多个
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

结果:

Service1:wxx
Service2:wxx
Service3:wxx
Service1:she
Service2:she
Service3:she

3 ThreadLocal原理

  • 首先我们应该明确如下类之间的关系:ThreadLocal、ThreadLocalMap、Thread。
  • ThreadLocalMap 是 ThreadLocal的内部类。ThreadLocalMap是一个存储键值对Map容器,ThreadLocalMap中还有内部类Entry,用于存储每个键值对,其中键为 ThreadLocal 变量,值为用户传入的对象。关系如下:

在这里插入图片描述

现在搞清楚了ThreadLocal、ThreadLocalMap之间的关系,那这两个和Thread是什么关系呢?答案是:Thread中有一个 ThreadLocal.ThreadLocalMap 的变量。如下图:

在这里插入图片描述

public class Thread implements Runnable {
    // ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // ....
}

接下来我们就可以探究ThreadLocal到底是如何获取属于线程内部的变量的,关键在于探究ThreadLocal的 get() 方法。该函数如下:

public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
}

该函数中使用到了 getMap 和 setInitialValue 两个函数,这两个函数的定义如下:

public class ThreadLocal<T> {
    private T setInitialValue() {
        T value = initialValue();  // 用法一 重写了该方法,由多态可知,返回重写的该函数的返回值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  // 得到当前线程t的成员变量 threadLocals
        if (map != null)
            map.set(this, value);  // 向 threadLocals 中放入键值对, 关键!!!
        else
            createMap(t, value);
        return value;
    }
    public void set(T value) {  // 用法二调用了该方法
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);  // 向 threadLocals 中放入键值对, 关键!!!
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

分析 get() 函数的执行流程:

(1)获取当前线程 t ,然后调用 getMap(t) ,从而得到属于当前线程 t 的ThreadLocalMap变量 map ;

(2)然后判断属于当前线程 t 的 map 是否为空,不空的话从 map 中取出当前键值对,这里的键是this,也就是说调用get()方法的变量。对应于用法一的 TSF.df ,对应于用法二的 UserContextHolder.holder 。为空的话则调用 setInitialValue() ,该函数会将this作为键,重写的 initialValue() 返回值作为值存入到 map 中。

(3)返回 this 对象对应的值。

无论是用法一,还是用法二,其实本质上都在操纵 当前线程 t 的成员变量 threadLocals 。

根据上述 get() 分析的第(2)点,当我们 new ThreadLocal<>(); 时并没有向 ThreadLocalMap 中存入键值对,只有当调用 get()、set() 方法时才放入键值对,这是懒加载的一种体现。

4 ThreadLocal注意点

ThreadLocalMap

  • ThreadLocalMap 和 HashMap 类似,关于 HashMap 的详细分析,可以参考:HashMap源码分析。
  • 两者也有不少区别:
    • 两者解决哈希冲突的方式不同;
    • ThreadLocalMap中的键值对,其中键为软引用,值为强引用,但HashMap中键值都为强引用。

解决哈希冲突

  • ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置;
  • HashMap采用拉链法(链表+红黑树)。

ThreadLocalMap中节点的键值对

如果弱引用对象只与弱引用关联,则这个弱引用对象可以被回收。

ThreadLocalMap中的Entry继承自WeakReference,是弱引用;

每一个Entry都是对key的弱引用;

每个Entry都包含了一个对value的强引用;

value为强引用的原因:因为JVM认为这个引用十分重要,是程序员定义的,不能随意回收,回收之后可能发生异响不到的错误;

因为值value是强引用,所以可能导致内存泄露,最终导致OOM,这是因为:如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,存在以下调用链:Thread---->ThreadLocalMap---->Entry(key为null)---->value。导致value无法回收,日积月累可能造成OOM。

JDK已经考虑到了这个问题,所以在Entry的set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收。但是这样做还不足够,因为我们必须调用这些方法才能达到上述效果。

为了避免产生内存泄露问题,我们在使用完ThreadLocal之后,就应该调用remove方法(阿里规约)。例如用法二中 Service3 应该改为:

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3:" + user.name);
        UserContextHolder.holder.remove();  // 防止内存泄露
    }
}

我们可不可以在新建ThreadLocal并在没有重写initialValue()方法后,直接调用 ThreadLocal 的 get()方法?

可以,只不过会返回 null 。

如下代码演示了上述描述的问题:

public class ThreadLocalNPE {
    ThreadLocal<Long> tl = new ThreadLocal<>();
//    public void set() {
//        tl.set(Thread.currentThread().getId());
//    }
    public long get() {  // 返回值改为 Long 就没有NPE异常了
        return tl.get();  // tl.get() 为 null
    }
    public static void main(String[] args) {
        ThreadLocalNPE main = new ThreadLocalNPE();
        // 不进行set,直接get
        main.get();
    }
}

上述代码会抛出java.lang.NullPointerException异常,这不是因为get()的原因,而是因为:拆箱时null不能转为基本类型。当返回值改为 Long 就没有NPE异常了。

到此这篇关于Java中ThreadLocal的用法及原理详解的文章就介绍到这了,更多相关ThreadLocal的用法及原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用stream的Collectors.toMap()方法常见的问题及解决

    使用stream的Collectors.toMap()方法常见的问题及解决

    这篇文章主要介绍了使用stream的Collectors.toMap()方法常见的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • 实例讲解JAVA设计模式之备忘录模式

    实例讲解JAVA设计模式之备忘录模式

    这篇文章主要介绍了JAVA设计模式之备忘录模式的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下
    2020-06-06
  • Java中Objects.equals踩坑记录

    Java中Objects.equals踩坑记录

    最近在工作中发现一个问题,觉着还是挺有必要记录下的,这篇文章主要给大家介绍了关于Java中Objects.equals踩坑的相关资料,需要的朋友可以参考下
    2022-03-03
  • 详解Java异常处理最佳实践及陷阱防范

    详解Java异常处理最佳实践及陷阱防范

    这篇文章主要介绍了Java异常处理最佳实践及陷阱防范,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04
  • Spring IOC简单理解及创建对象的方式

    Spring IOC简单理解及创建对象的方式

    这篇文章主要介绍了Spring IOC简单理解及创建对象的方式,本文通过两种方式给大家介绍创建对象的方法,通过实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2021-09-09
  • Springboot+Mybatis实现分页加条件查询功能

    Springboot+Mybatis实现分页加条件查询功能

    这篇文章主要为大家详细介绍了Springboot+Mybatis实现分页加条件查询,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • idea如何关闭页面显示的浏览器图标

    idea如何关闭页面显示的浏览器图标

    这篇文章主要介绍了idea如何关闭页面显示的浏览器图标问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • Nexus存储库管理器搭建-Maven私服详解

    Nexus存储库管理器搭建-Maven私服详解

    这篇文章主要介绍了Nexus存储库管理器搭建-Maven私服,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-06-06
  • 在SpringBoot接口中正确地序列化时间字段的方法

    在SpringBoot接口中正确地序列化时间字段的方法

    文章主要介绍在 Spring Boot 接口中正确序列化时间字段的方法,包括 Java 中Date和LocalDateTime类型的区别,JSON 序列化和请求参数中时间字段的处理,如时间字符串的格式配置、时间戳的使用及相关配置,还提到了在 Swagger UI 中的类型设置,需要的朋友可以参考下
    2024-11-11
  • Java RPC框架熔断降级机制原理解析

    Java RPC框架熔断降级机制原理解析

    这篇文章主要介绍了Java RPC框架熔断降级机制原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02

最新评论