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

相关文章

Python实现图片分割的多种方法总结

《Python实现图片分割的多种方法总结》图片分割是图像处理中的一个重要任务,它的目标是将图像划分为多个区域或者对象,本文为大家整理了一些常用的分割方法,大家可以根据需求自行选择... 目录1. 基于传统图像处理的分割方法(1) 使用固定阈值分割图片(2) 自适应阈值分割(3) 使用图像边缘检测分割(4)

Android实现在线预览office文档的示例详解

《Android实现在线预览office文档的示例详解》在移动端展示在线Office文档(如Word、Excel、PPT)是一项常见需求,这篇文章为大家重点介绍了两种方案的实现方法,希望对大家有一定的... 目录一、项目概述二、相关技术知识三、实现思路3.1 方案一:WebView + Office Onl

Java中Switch Case多个条件处理方法举例

《Java中SwitchCase多个条件处理方法举例》Java中switch语句用于根据变量值执行不同代码块,适用于多个条件的处理,:本文主要介绍Java中SwitchCase多个条件处理的相... 目录前言基本语法处理多个条件示例1:合并相同代码的多个case示例2:通过字符串合并多个case进阶用法使用

Java中的Lambda表达式及其应用小结

《Java中的Lambda表达式及其应用小结》Java中的Lambda表达式是一项极具创新性的特性,它使得Java代码更加简洁和高效,尤其是在集合操作和并行处理方面,:本文主要介绍Java中的La... 目录前言1. 什么是Lambda表达式?2. Lambda表达式的基本语法例子1:最简单的Lambda表

Java中Scanner的用法示例小结

《Java中Scanner的用法示例小结》有时候我们在编写代码的时候可能会使用输入和输出,那Java也有自己的输入和输出,今天我们来探究一下,对JavaScanner用法相关知识感兴趣的朋友一起看看吧... 目录前言一 输出二 输入Scanner的使用多组输入三 综合练习:猜数字游戏猜数字前言有时候我们在

C# foreach 循环中获取索引的实现方式

《C#foreach循环中获取索引的实现方式》:本文主要介绍C#foreach循环中获取索引的实现方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、手动维护索引变量二、LINQ Select + 元组解构三、扩展方法封装索引四、使用 for 循环替代

Spring Security+JWT如何实现前后端分离权限控制

《SpringSecurity+JWT如何实现前后端分离权限控制》本篇将手把手教你用SpringSecurity+JWT搭建一套完整的登录认证与权限控制体系,具有很好的参考价值,希望对大家... 目录Spring Security+JWT实现前后端分离权限控制实战一、为什么要用 JWT?二、JWT 基本结构

java解析jwt中的payload的用法

《java解析jwt中的payload的用法》:本文主要介绍java解析jwt中的payload的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java解析jwt中的payload1. 使用 jjwt 库步骤 1:添加依赖步骤 2:解析 JWT2. 使用 N

springboot项目如何开启https服务

《springboot项目如何开启https服务》:本文主要介绍springboot项目如何开启https服务方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录springboot项目开启https服务1. 生成SSL证书密钥库使用keytool生成自签名证书将

Java实现优雅日期处理的方案详解

《Java实现优雅日期处理的方案详解》在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间,下面我们就来看看如何使用java处理这样的日期问题吧,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言一、日期的坑1.1 日期格式化陷阱1.2 时区转换二、优雅方案的进阶之路2.1 线程安全重构2