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的用法及原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解Spring boot使用Redis集群替换mybatis二级缓存

    详解Spring boot使用Redis集群替换mybatis二级缓存

    本篇文章主要介绍了详解Spring boot使用Redis集群替换mybatis二级缓存,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-05-05
  • Spring Boot项目传参校验的最佳实践指南

    Spring Boot项目传参校验的最佳实践指南

    有参数传递的地方都少不了参数校验,在web开发中前端的参数校验是为了用户体验,后端的参数校验是为了安全,下面这篇文章主要给大家介绍了关于Spring Boot项目传参校验的最佳实践,需要的朋友可以参考下
    2022-04-04
  • Spring mvc Controller和RestFul原理解析

    Spring mvc Controller和RestFul原理解析

    这篇文章主要介绍了Spring mvc Controller和RestFul原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • java中this的n种使用方法

    java中this的n种使用方法

    this可能是几乎所有有一点面向对象思想的语言都会引用到的变量,this有多少种用法。下面小编给大家带来了java中this的n种使用方法,感兴趣的朋友一起看看吧
    2018-08-08
  • Java动态代理模式的深入揭秘

    Java动态代理模式的深入揭秘

    这篇文章主要给大家介绍了关于Java动态代理模式的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Java具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-08-08
  • Java基本语法之内部类示例详解

    Java基本语法之内部类示例详解

    本文带大家认识Java基本语法——内部类,将一个类定义放在另一类的定义的内部,这个就是内部类,内部类允许将一些逻辑相关的类组织在一起,并能够控制位于内部的类的可视性,感兴趣的可以了解一下
    2022-03-03
  • SpringBoot服务端数据校验过程详解

    SpringBoot服务端数据校验过程详解

    这篇文章主要介绍了SpringBoot服务端数据校验过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • 浅析Java验证码生成库JCaptcha

    浅析Java验证码生成库JCaptcha

    JCaptcha 是一个用来生成验证码的开源Java类库,使用起来也是非常的简单方便。本文通过代码实例介绍了JCaptcha类库。
    2016-07-07
  • SpringBoot实现接口返回数据脱敏的代码示例

    SpringBoot实现接口返回数据脱敏的代码示例

    在当今的信息化时代,数据安全尤为重要,接口返回数据脱敏是一种重要的数据保护手段,可以防止敏感信息通过接口返回给客户端,本文旨在探讨如何在SpringBoot应用程序中实现接口返回数据脱敏,需要的朋友可以参考下
    2024-07-07
  • java将指定目录下文件复制到目标文件夹的几种小方法

    java将指定目录下文件复制到目标文件夹的几种小方法

    在Java中有多种方法可以实现文件的复制,这篇文章主要给大家介绍了关于java将指定目录下文件复制到目标文件夹的几种小方法,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-01-01

最新评论