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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

变量与命名

引言         在前两个课时中,我们已经了解了 Python 程序的基本结构,学习了如何正确地使用缩进来组织代码,并且知道了注释的重要性。现在我们将进一步深入到 Python 编程的核心——变量与命名。变量是我们存储数据的主要方式,而合理的命名则有助于提高代码的可读性和可维护性。 变量的概念与使用         在 Python 中,变量是一种用来存储数据值的标识符。创建变量很简单,