Java是如何通过ThreadLocal类来实现变量的线程独享

2023-11-20 23:30

本文主要是介绍Java是如何通过ThreadLocal类来实现变量的线程独享,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一 概述

Java中,如果一个变量要被多线程访问,可以使用volatile关键字将它声明为“易变的”;如果一个变量只要被某个线程独享时,我们可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组ThreadLocal<?>的实例化对象为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量。

二 ThreadLocal,ThreadLocalMap和Thread的关系

ThreadLocal,ThreadLocalMap,Thread的关系图(图一):

Thread,ThreadLocal和ThreadLocalMap相关的源码:

//Thread类
public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;
}//ThreadLocal类与ThreadLocalMap类
public class ThreadLocal<T> {static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;//可能导致内存泄漏Entry(ThreadLocal<?> k, Object v) {//弱引用super(k);//强引用value = v;}}private static final int INITIAL_CAPACITY = 16;private Entry[] table;
}

三 ThreadLocal的使用场景与实例

场景一:每个线程都需要一个独享的对象,同时使用该对象是线程安全的,如SimpleDateFormat本身在多线程环境下不是线程安全的,我们利用ThreadLocal使得对象为一个线程独享,从而变得线程安全。

代码实例一:借助ThreadLocal通过大小为10的线程池完成1000个线程使用线程非安全的SimpleDateFormat类

/*** 描述:利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存*/
public class ThreadLocalExample {public static ExecutorService threadPool = Executors.newFixedThreadPool(10);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000; i++) {int time = i;threadPool.submit(new Runnable() {@Overridepublic void run() {String date = new ThreadLocalExample().date(time);}});}threadPool.shutdown();}public String date(int seconds) {//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时Date date = new Date(1000 * seconds);SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();return dateFormat.format(date);}
}//ThreadLocal是可以并行的class ThreadSafeFormatter {public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new                 ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}};public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

此时每个线程中的ThreadLocalMap中的key为ThreadLocal<SimpleDateFormat>实例化对象,value为SimpleDateFormat的对象实例。

场景二:每个对象中需要保存全局变量,使得统一请求中或同一线程中不同方法直接使用共享的变量,避免同一个参数被多次传递

代码实例二:通过ThreadLocal对象使得某个变量可以在同一个线程中被线程中的多个方法安全的共享。

public class ThreadLocalExample {public static void main(String[] args) {new Service1().process("");}
}class Service1 {public void process(String name) {User user = new User("ThreadLocal Example");//将变量保存在ThreadLocal对象中UserContextHolder.holder.set(user);new Service2().process();}
}class Service2 {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);UserContextHolder.holder.remove();}
}class UserContextHolder {public static ThreadLocal<User> holder = new ThreadLocal<>();
}class User {String name;public User(String name) {this.name = name;}
}

此时每个线程中的ThreadLocalMap中的key为ThreadLocal<User>实例化对象,value为SimpleDateFormat的对象实例。

三 ThreadLocal中的重要方法

initialValue()

    protected T initialValue() {return null;}

initialValue()方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get方法的时候才会触发,当线程第一次使用get方法访问变量时,将调用此方法,当线程先调用了set方法的情况下,不会为线程调用本身的initalValue()方法。

如果不重写该方法默认情况下会返回null。一般如场景一同样使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

get()

    public T get() {//获取当前线程Thread t = Thread.currentThread();//获取ThreadLocalMap,每个线程都拥有一个ThreadLocalMap类的成员变量ThreadLocalMap map = getMap(t);if (map != null) {//this表示将当前的ThreadLocal对象作为key获取对应的value对象ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {//result为我们的目标对象,如场景一中的SimpleDateFormat对象和场景二中的User对象@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}

get()是先取出当前线程的ThreadLocalMap实例,然后调用map.getEntry方法,将该ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value,而且这个map中的key和value都是保存在当前线程中。

ThreadLocalMap类似于HashMap,不同于HashMap的是处理hash碰撞的方式,前者是采用线性探测法,即当发生hash冲突的时候就继续找下一个空位置,而后者是采用拉链法,当发生hash冲突之后就会采用链表存储,在Java8开始当链表长度超过8之后就使用红黑树进行存储。

四 ThreadLocal中的内存泄露问题

      static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;//可能导致内存泄漏Entry(ThreadLocal<?> k, Object v) {//弱引用super(k);//强引用value = v;}}

由上述代码可知,ThreadLocalMap中的value和Thread之间存在强引用链路(JVM中只要强引用关系存在,垃圾收集器就永远不会回收掉被引用的对象),所以会导致value对象无法被正常回收,可能会出现OOM。基于这种情况,JDK进行了相应的处理,即在使用set,remove,rehash方法的时候扫描key为null的entry,并把对应的value设置成null,这样value对象就可以被回收。

问题是如果一个ThreadLocal对象不再被使用了,那么set,remove,rehash方法也不会被调用,如果同时线程又停止了,那么强引用链就会一致存在,就会导致内存泄漏。

阿里规约中写到,调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法。

五 ThreadLocal注意点

ThreadLocal存在其好处,但是并不需要强行使用,如在任务数很少的时候,在局部变量中可以新建对象解决问题,就不需要使用ThreadLocal来解决问题。

如果每个线程中ThreadLocal.set()的对象本身就是多线程共享,如static对象,那么多线程的ThreadLocal.get()取得的还是这个共享对象的本身,就会出现并发访问的问题。

我们应该善于使用框架中成熟的ThreadLocal方案,如Spring中的RequestContestHolder,DateTimeContextHolder,这样可以减少我们的维护工作。

RequestContextHolder

public abstract class RequestContextHolder  {private static final ThreadLocal<RequestAttributes> requestAttributesHolder =new NamedThreadLocal<>("Request attributes");private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =new NamedInheritableThreadLocal<>("Request context");
}

DateTimeContextHolder

public final class DateTimeContextHolder {private static final ThreadLocal<DateTimeContext> dateTimeContextHolder =new NamedThreadLocal<>("DateTimeContext");
}

每一个Http请求都对应一个线程,线程之间是相互隔离的,这种情况就是ThreadLocal的典型应用场景。

六 父子进程可共享的ThreadLocal实现

ThreadLocal是一个父子进程不能共享的线程独享实现方式,如果想要在父子线程之间进行共享可以使用InheritableThreadLocal类来实现此功能。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {//父线程创建子线程时,向子线程复制InheritableThreadLocal变量时用protected T childValue(T parentValue) {return parentValue;}//重写getMap,操作InheritableThreadLocal时,将于threadLocals变量无关,只会影响Thread类中的inheritableThreadLocals变量ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}//类似getMapvoid createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

Thread类中的inheritableThreadLocals变量

/*Thread类中的变量inheritableThreadLocals继承了父线程的ThreadLocalMap,
用于父子进程之间ThreadLocal变量的传递,即inheritableThreadLocals主要存储
可自动向子进程传递的ThreadLocal.ThreadLocalMap.*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Thread的初始化方法init(...)

public class Thread implements Runnable {private void init(ThreadGroup g, Runnable target, String name,long stackSize) {init(g, target, name, stackSize, null, true);} private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;Thread parent = currentThread();SecurityManager security = System.getSecurityManager();if (g == null) {/* Determine if it's an applet or not *//* If there is a security manager, ask the security managerwhat to do. */if (security != null) {g = security.getThreadGroup();}/* If the security doesn't have a strong opinion of the matteruse the parent thread group. */if (g == null) {g = parent.getThreadGroup();}}/* checkAccess regardless of whether or not threadgroup isexplicitly passed in. */g.checkAccess();/** Do we have the required permissions?*/if (security != null) {if (isCCLOverridden(getClass())) {security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);}}g.addUnstarted();this.group = g;this.daemon = parent.isDaemon();this.priority = parent.getPriority();if (security == null || isCCLOverridden(parent.getClass()))this.contextClassLoader = parent.getContextClassLoader();elsethis.contextClassLoader = parent.contextClassLoader;this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();this.target = target;setPriority(priority);if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/* Stash the specified stack size in case the VM cares */this.stackSize = stackSize;/* Set thread ID */tid = nextThreadID();}
}

Thread类中的init(...)方法有两个实现,差别为init(ThreadGroup g, Runnable target, String name,long stackSize) 未传入参数AccessControlContext和inheritThreadLocals默认为true,这种情况下父线程inheritableThreadLocals不为空时就会将父线程的inheritablethreadLocals传递至子线程。而init(ThreadGroup g, Runnable target,String name,long stackSize, AccessControlContext acc, boolean inheritThreadLocals)传入了AccessControlContext而且inheritThreadLocals变量默认为false。

ThreadLocal的createInheritedMap()方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);}/*构建一个包含所有parentMap中Inheritable ThreadLocalsd ThreadLocals的ThreadLocalMap*/
private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);//使用Entry数组存放ThreadLocalMap中的ThreadLocaltable = new Entry[len];//逐一复制parentMap中的记录for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {//从子线程中的ThreadLocalMap中获取指定的变量Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}
}

根据ThreadLocalMap(ThreadLocalMap parentMap)方法可知,子线程将父线程的ThreadLocalMap中的值逐一复制到本身。

这篇关于Java是如何通过ThreadLocal类来实现变量的线程独享的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/398217

相关文章

HTML5 getUserMedia API网页录音实现指南示例小结

《HTML5getUserMediaAPI网页录音实现指南示例小结》本教程将指导你如何利用这一API,结合WebAudioAPI,实现网页录音功能,从获取音频流到处理和保存录音,整个过程将逐步... 目录1. html5 getUserMedia API简介1.1 API概念与历史1.2 功能与优势1.3

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

springboot项目中整合高德地图的实践

《springboot项目中整合高德地图的实践》:本文主要介绍springboot项目中整合高德地图的实践,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一:高德开放平台的使用二:创建数据库(我是用的是mysql)三:Springboot所需的依赖(根据你的需求再

spring中的ImportSelector接口示例详解

《spring中的ImportSelector接口示例详解》Spring的ImportSelector接口用于动态选择配置类,实现条件化和模块化配置,关键方法selectImports根据注解信息返回... 目录一、核心作用二、关键方法三、扩展功能四、使用示例五、工作原理六、应用场景七、自定义实现Impor

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项

使用Python和OpenCV库实现实时颜色识别系统

《使用Python和OpenCV库实现实时颜色识别系统》:本文主要介绍使用Python和OpenCV库实现的实时颜色识别系统,这个系统能够通过摄像头捕捉视频流,并在视频中指定区域内识别主要颜色(红... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间详解

PostgreSQL中MVCC 机制的实现

《PostgreSQL中MVCC机制的实现》本文主要介绍了PostgreSQL中MVCC机制的实现,通过多版本数据存储、快照隔离和事务ID管理实现高并发读写,具有一定的参考价值,感兴趣的可以了解一下... 目录一 MVCC 基本原理python1.1 MVCC 核心概念1.2 与传统锁机制对比二 Postg

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri