Java并发工具之ThreadLocal

news2025/1/12 20:54:16

一、ThreadLocal 简介

1. ThreadLocal 是什么?

ThreadLocal 字面意思是本地线程,其实更准确来说是线程局部变量,线程类 Thread 有个变量叫做 threadLocals,其类型就是ThreadLocal.ThreadLocalMap 类型,他其实不是一个 Map 类型,但可以暂时理解它是一个Map,键为 ThreadLocal 对象,值就是要存入的value。

2. ThreadLocal 作用

ThreadLocal 就是用于线程间的数据隔离的。ThreadLocal 提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程,获取保存的值时非常方便,ThreadLocal 为变量在每个线程中都创建了一个副本,每个线程就可以很方便的访问自己内部的副本变量。

3. ThreadLocal的两大使用场景

  • 每个线程需要一个独享的对象(通常是指工具类对象),每个线程内有自己的实例副本,不与其他线程共享;
  • 每个线程内需要一个变量作为全局共用(当前线程内全局共用),可以让不同的方法直接使用,避免传递参数的麻烦;

总之,就是解决多个线程的共享变量的线程安全问题;

二、使用场景案例

1. 每个线程需要一个独享的对象

下面的案例是关于 SimpleDateFormat 工具类,在多线程共享时的线程安全问题

线程不安全的代码:

public class MyThreadLocal {

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

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        return sdf.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}

这里为了提高性能,所以将 SimpleDateFormat 作为 static 属性,多线程共享,但是这样就会出现安全问题,打印结果如下:

image-20230608083233479

由结果可以看出,打印出了两个相同的时间,说明发生了运行结果错误,问题代码就发生在sdf.format(date),这行代码不是线程安全的。

解决方案有两个:使用同步锁 synchronized 和使用 ThreadLocal 解决。

(1)同步锁 synchronized

public class MyThreadLocal {

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

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (MyThreadLocal.class) {
            s = sdf.format(date);
        }
//        Date date = new Date(1000 * seconds);
        return s;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}

(2) 用ThreadLocal解决

public class MyThreadLocal1 {

//    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat sdf = simpleDateFormatThreadLocal.get();
        return sdf.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}

打印结果:

两个方案都可以解决线程安全问题,但是synchronized加锁的方法,由于同一时刻只有一个线程执行,所以效率低下;ThreadLocal方法在多线程并行的情况下,由于每个线程内都有自己独享的对象,也不会有线程安全问题。

2. 每个线程内需要一个变量作为全局共用

在应用开发中,有些参数需要被线程内许多方法使用,如权限管理,很多的方法都需要验证当前线程用户的身份信息

案例内容:一个系统中,user对象需要在很多server中进行使用

  • 方案1
    将user作为参数层层传递,从service1->service2->service3以此类推。这样会导致代码冗余且难以维护

  • 方案2
    定义一个全局的static 的user,想要拿的时候直接获取。但这是一种错误的方案!!因为我们现在的场景是多用户的系统,每个线程对应着不同的用户,每个线程的user是不同的

  • 方案3
    定义一个UserMap,每次访问从Map中获取用户的信息,多线程访问下加锁或者使用ConcurrentHashMap,但是对性能有影响

  • 方案4
    利用ThreadLocal,不需要锁,不影响性能。ThreadLocal 主打的就是同一个线程内不同方法间的共享。

所以优选选择方案4,代码演示如下:

/**
 * 避免传递参数的麻烦
 * ThreadLocalan案例2
 * @author Chkl
 * @create 2020/3/10
 * @since 1.0.0
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("周星驰");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("service2:" + user.name);
        UserContextHolder.holder.remove();
        UserContextHolder.holder.set(new User("古天乐"));
        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;
    }
}

打印结果:

通过以上两个案例,我们可以了解到 ThreadLocal 的两个作用:

  • 让对象在线程之间隔离;
  • 在任何方法中都可以直接获取到对象;

3. ThreadLocal 的两个使用方式

上面两个使用场景中,ThreadLocal 的初始化方式也是分为两种:

  • 场景1:initialValue
    如果在ThreadLocal第一次get的时候把对象给初始化时使用,对象的初始化时机受控制
  • 场景2:set
    如果需要保存到ThreadLocal的对象的生成时机不由我们随意控制,我们用set方法放进去,再用get方法取出来;

4. ThreadLocal的好处

  • 线程安全

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

  • 更高效的利用内存,节省开销

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

  • 避免传参的繁琐操作

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

三、ThreadLocal 原理

1. ThreadLocal 与 Thread 的关系

  • 每一个Thread里面都有一个ThreadLocalMap类型的threadlocals成员变量,它可以存储很多的ThreadLocal对象,因为一个线程可能有多个ThreadLocal对象,其中对象引用名称作为key;
  • ThreadLocalMap:也就是Thread.threadLocals,是Thread里的一个成员变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对;键:这个ThreadLocal;值:实际需要的成员变量;

2. ThreadLocal 源码

(1) initialValue() 方法源码

/**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }
  • 该方法返回当前线程对应的初始值,使用了延迟加载,当调用get()方法是才会触发
  • 当第一次使用get()方法时会调用此方法,如果调用前用set()方法设置了值就不会调用
  • 当调用remove()方法后再次调用get()方法依然会调用initialize
  • 如果不重写initialValue方法,直接调用get()会返回null

(2) get 方法的实现:

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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(t) 方法获取 ThreadLocal.ThreadLocalMap,所有的变量数据都存在该 map,map 的具体类型是一个 Entry 数组。

然后接着下面获取到 Entry 键值对,注意这里获取 Entry 时参数传进去的是 this,即 ThreadLocal 实例,而不是当前线程 t。如果获取成功,则返回 value 值。

如果 map 为空,则调用 setInitialValue 方法返回一个初始 value,其实这个默认初始 value 为 null。

(3) 接着来看一下 getMap 方法做了什么:

	/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
	ThreadLocal.ThreadLocalMap threadLocals = null;
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

在 getMap 中,是调用当期线程 t,返回当前线程t中的一个成员变量 threadLocals,类型为 ThreadLocal.ThreadLocalMap。就是上面提到的每一个线程都自带一个 ThreadLocalMap 类型的成员变量。

(4) 继续来看 ThreadLocalMap 的实现:

	static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,其内部主要是一个 Entry 数组存储数据(并不是一个 map 类型)。

ThreadLocalMap 的 Entry 继承了 WeakReference,用来实现弱引用,被弱引用关联的对象(其实就是 ThreadLocal 对象)只能生存到下一次垃圾收集发生之前,并且使用 ThreadLocal 对象的 HashCode 的散列值计算得出的 Entry 数组的下标 i,这里不同对象可能存在相同的下标 i,对此 set() 方法处理逻辑是:下标加一,直到第一个要插入的位置为空。

(5) set()方法

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

为这个线程设置一个新值

(6) remove()方法

/**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

删除线程中对应的值,remove()方法也是在ThreadlocalMap中进行操作,传入当前ThreadLocal对象的引用,删除map中的value的值,不是删除整个ThreadLocalMap对象,而是根据this(也就是当前ThreadLocal对象)来删除对应的threadLocal对象

四、ThreadLocal 注意点

1. 在使用 ThreadLocal时的注意事项

  • 最后一次使用之后应该手动的调用remove()方法,防止内存泄露
  • 如果可以不使用ThreadLocal就解决问题,不要强行使用(如:任务数很少时)
  • 优先使用框架的支持,而不是自己创造,例如在Spring中,如果可以使用 RequestContextHolder,那么就不需要自己去维护ThreadLocal,因为自己可能会忘记调用remove方法,造成内存泄漏;

2 ThreadLocal 为什么会发生内存泄露?

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

源码:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

我们知道,ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry 中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。

如果一个线程调用 ThreadLocal 的 set 设置变量,当前 ThreadLocalMap 则新增一条记录,但发生一次垃圾回收,此时 key 值被回收,而 value 值依然存在内存中,如果线程一直存在(比如在线程池中),那么 value 值将一直被引用,不能被回收。因为存在一条引用链的关系:Thread–>ThreadLocalMap–>Entry–>Value。造成内存泄漏,甚至有可能造成内存溢出OOM

如何避免内存泄漏:当使用完了对应的ThreadLocal,主动调用remove方法删除。

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();
    }
}

打印结果:

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

ThreadLocal在实例化时是指定存储的包装类型 Long (ThreadLocal tl = new ThreadLocal()), 而演示代码中的 get() 方法返回的是基本类型 long,那么他在执行 initialValue() 时返回的是 Long,然后自动拆箱,转为 long 基本类型,这里就出现了错误,因为在返回Long 类型时就是null了,对 null进行拆箱返回基本类型,就会出现空指针这异常!

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

点我扫码关注微信公众号

文章来源:Java并发工具之ThreadLocal


个人微信:CaiBaoDeCai

微信公众号名称:Java知者

微信公众号 ID: JavaZhiZhe

谢谢关注!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/643516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

回忆童年游戏,完美利用Python制作飞机大战(爷青回~)

名字&#xff1a;阿玥的小东东 学习&#xff1a;python、C/C 博客链接&#xff1a;阿玥的小东东的博客_CSDN博客-python&&c高级知识,过年必备,C/C知识讲解领域博主 目录 pygame包的安装 添加python环境变量 创建飞机大战窗口 飞机照片 将变量及函数抽取出来 添加多…

LeetCode:31. 下一个排列

31. 下一个排列 1&#xff09;题目2&#xff09;思路3&#xff09;代码4&#xff09;结果 1&#xff09;题目 整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。 例如&#xff0c;arr [1,2,3] &#xff0c;以下这些都可以视作 arr 的排列&#xff1a;[1,2,3]、[1…

Nacos负载均衡策略

文章目录 按权重分配流量负载均衡自定义负载均衡策略 按权重分配流量负载均衡 SpringCloud新版本&#xff08;2021.x.x&#xff09;中负载均衡器用LoadBalancer替代了Ribbon&#xff0c;默认只提供了2种负载均衡策略&#xff1a;RandomLoadBalancer 和 RoundRobinLoadBalancer。…

[冷冻电镜]IMOD使用指南

参考教程&#xff1a; Etomo Tuturial for IMOD version 4.11 1. Initial Setup 本教程提供了一个小双轴示例数据集和Etomo的分布指南&#xff0c;更详细的内容参考Tomography Guide。该版本使用1k*1k的图像而不是压缩版本。imodhelp命令可以打开帮助界面&#xff0c;查看各种…

java并发之CAS(Compare and swap)

1. 简介 CAS的底层调用native方法&#xff0c;最终是利用CPU的一个特殊指令&#xff0c;该指令由CPU保证了原子性&#xff0c;而且包含多个操作&#xff0c;比如先比较再更新。 原理&#xff1a; &#xff08;1&#xff09;需要读写的内存值&#xff08;V&#xff09;、原值…

【Kafka】Docker安装kafkajava kafka api

内容目录 一、安装zookeeper1 拉取镜像2 创建network3 启动容器 二、安装kafka1 拉取kafka镜像2 启动kafka容器3 创建topic4 创建生产者5 创建消费者 三、kafka的java api1 producer2 消费者 docker依赖于zookeeper&#xff0c;首先安装zookeeper 一、安装zookeeper 1 拉取镜像…

[PyTorch][chapter 40][数据增强]

前言&#xff1a; 深度学习对数据量要求非常大, 我们通常会遇到图像的数据集比较小,影响Train效果。 这个时候可以通过transformer 方法,增加图像的多样性,达到数据 增强的效果。 transformer 不会单独使用&#xff0c;通常和其它torch 其他类一起使用 transformer 常用方法…

可视管理 数字孪生智慧隧道一体化管控平台

前言 交通是国家发展的关键&#xff0c;四通八达的交通路线&#xff0c;对国家经济、社会等方面的发展起着至关重要的作用。 建设背景 随着社会经济的持续发展与城市化进程的平稳推进&#xff0c;我国公路工程规模逐步扩大&#xff0c;公路工程建设直接影响着城市未来发展与…

Vue 报错 error:0308010C:digital envelope routines::unsupported

症状 Vue 报错error:0308010C:digital envelope routines::unsupported 原因 出现这个错误是因为 node.js V17版本中最近发布的OpenSSL3.0, 而OpenSSL3.0对允许算法和密钥大小增加了严格的限制&#xff0c;可能会对生态系统造成一些影响. 解决方法 方法1 打开终端&#x…

React 应用 Effect Hook 函数式中操作生命周期

React Hook入门小案例 在函数式组件中使用state响应式数据给大家演示了最简单的 Hook操作 那么 我们继续 首先 Hook官方介绍 他没有破坏性是完全可选的 百分比兼容 也就说 我们一起的 类 class的方式也完全可以用 只要 react 16,8以上就可以使用 Hook本身不会影响你的react的理…

ESXi 7.0 U3m Hitachi (日立) 定制版 OEM Custom Installer CD

VMware ESXi 7.0 Update 3m - 领先的裸机 Hypervisor (All OEM Customized Installer CDs) ESXi 7.0 U3m Standard (标准版) ESXi 7.0 U3m Dell (戴尔) 定制版 OEM Custom Installer CD ESXi 7.0 U3m HPE (慧与) 定制版 OEM Custom Installer CD ESXi 7.0 U3m Lenovo (联想) 定…

4.单表查询

SQL句子中语法格式提示&#xff1a; 1.中括号&#xff08;[]&#xff09;中的内容为可选项&#xff1b; 2.[&#xff0c;...]表示&#xff0c;前面的内容可重复&#xff1b; 3.大括号&#xff08;{}&#xff09;和竖线&#xff08;|&#xff09;表示选择项&#xff0c;在选择…

chatgpt赋能python:Python怎么导入第三方库

Python怎么导入第三方库 如果你是Python开发者&#xff0c;你一定会使用各种第三方库来加速你的开发过程。这些库可能是Python标准库之外的代码&#xff0c;或由其他人编写的自定义代码。使用这些库可以让你的开发更高效、更易于管理&#xff0c;并且可以避免重复造轮子。 但…

RabbitMQ虚拟主机无法启动的原因和解决方案

RabbitMQ虚拟主机无法启动的原因和解决方案 摘要&#xff1a; RabbitMQ是一个广泛使用的开源消息代理系统&#xff0c;但在使用过程中可能会遇到虚拟主机无法启动的问题。本文将探讨可能导致该问题的原因&#xff0c;并提供相应的解决方案&#xff0c;以帮助读者解决RabbitMQ虚…

Learning C++ No.31 【线程库实战】

引言&#xff1a; 北京时间&#xff1a;2023/6/11/14:40&#xff0c;实训课中&#xff0c;实训场地有空调&#xff0c;除了凳子坐着不舒服之外&#xff0c;其它条件都挺好&#xff0c;主要是我带上了我自己的小键盘&#xff0c;并且教室可以充电&#xff0c;哈哈哈&#xff0c…

在做自动化测试之前你需要知道的

B站视频教程&#xff1a;Python自动化测试&#xff1a;7天练完这60个实战项目&#xff0c;年薪过35w。 什么是自动化测试&#xff1f; 做测试好几年了&#xff0c;真正学习和实践自动化测试一年&#xff0c;自我感觉这一个年中收获许多。一直想动笔写一篇文章分享自动化测试实践…

信息系统管理工程师-学习笔记1-信息化知识

考点1 信息与信息系统 信息的概念 信息的定义: 是有别与物质与能量的第三种东西,是对事物运动状态或存在方式的不确定行的描述 信息是按特定方式组织在一起的客体属性的集合,具有超出这些客体属性本身之外的价值两层次 1.本体论层次 : 纯客观的层次,只与客体本身的因素有关,与主…

python cv2的一些操作,如膨胀,画线,滤波等

目录 0. cv2简介1. 打开摄像头2. 画图,画线3. 滤波4. 获取角点5. 梯度边缘6. 图形匹配7. 形态学变化-膨胀腐蚀8. 二值化阈值10. 总结 0. cv2简介 在这里先简单介绍一下cv2吧。 cv2 是 OpenCV Python 库的主要模块&#xff0c;提供了许多图像处理和计算机视觉方面的函数和工具。…

vue2组件通信

父传子 传递静态或动态 Prop <!-- 传入静态值 --> <blog-post title"hai hai hai"></blog-post><!-- 传入变量值 --> <blog-post :title"info.title"></blog-post>传入一个对象的所有 property 数据 post: {id: 1…

进程管道:popen函数实例

基础知识 可能最简单的在两个程序之间传递数据的方法就是使用popen和pclose函数了。它们的原型如下所示&#xff1a; #include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream); 1&#xff0e;popen函数 popen函数允许一个程…