java.lang.ThreadLocal实现原理分析

本文深入解析了Java中的ThreadLocal机制,介绍了线程私有变量的概念,详细分析了ThreadLocal的源码实现,包括initialValue、get、set和remove等方法的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、简介

ThreadLocal隶属java.lang包,表示线程私有变量,也可叫做线程本地变量。它为单个线程单独创立了一个副本,每个线程只可访问属于自己的变量,不可访问和修改别的线程所属的变量。
ThreadLocal属于一个泛型类,泛型参数为变量的类型,可以通过重写initialValue方法来实现对该变量初始值的设置。
ThreadLocal提供了以下API实现对变量的控制:

public T get(); //返回该变量
public void set(T value); //设置该变量
public void remove(); //移除该变量

例如下面的代码:

public class Test {

	private static ThreadLocal<Integer> tl = new ThreadLocal<Integer>() {
		//重写设置初始值的方法
		@Override
		protected Integer initialValue() {
	        return 0;
	    }
	};

	public static void main(String[] args) {
		System.out.println(tl.get());
		tl.set(tl.get() + 1);
		System.out.println(tl.get());
		
		Thread t = new Thread() {
			public void run() {
				System.out.println(tl.get());
			}
		};
		
		t.start();
	}
}

输出为:

0
1
0

看到这里想必大家理解了ThreadLocal的使用方法及其作用了吧。

二、源码分析

ThreadLocal的主要方法有:

public T get(); //返回该变量
public void set(T value); //设置该变量
public void remove(); //移除该变量
protected T initialValue(); //返回变量的初始值
(1)initialValue方法

initialValue方法的作用是为变量设置初始值,该方法的默认实现是:

protected T initialValue() {
	return null;
}

如果想要为该变量设置一个初始值,只需重写该方法即可,例如:

@Override
protected String initialValue() {
	return "hello world";
}

ThreadLocal对象中很多方法的实现会调用该方法来设置初始值。

(2)get方法分析

想要了解ThreadLocal运作机制,我们可以从get方法作为切入点分析。

public T get() {
	Thread t = Thread.currentThread();
	//获取当前线程对象t对应的ThreadLocalMap
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		//以当前ThreadLocal对象为键查找值
		ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T)e.value;
			return result;
		}
	}
	return setInitialValue();
}

首先是获取当前运行线程对象,然后根据该线程对象找到对应的ThreadLocalMapThreadLocalMapThreadLocal的一个default访问权限内部类,其作用我们稍后分析)。如果找到了该线程对应的ThreadLocalMap,则通过当前ThreadLocal对象作为键查找Map中对应的Entry(键值对)对象,如果查找结果不为null,则返回Entry对象的value。否则调用setInitialValue方法将当前ThreadLocal对象(this)和变量作为键值对存入ThreadLocalMap并返回变量。

getMap方法:

ThreadLocalMap getMap(Thread t) {
	return t.threadLocals;
}

该方法很简单,即返回Thread对象的成员变量threadLocals。
查看java.lang.Thread对象源码:

ThreadLocal.ThreadLocalMap threadLocals = null;

相信很多人看到这里已经大致明白:每个Thread对象会持有一个ThreadLocalMap,当我们创建一个ThreadLocal对象时,该ThreadLocal对象本身作为键,变量作为值存入该ThreadLocalMap中,来实现线程的私有变量。

下面再来看setInitialValue方法的实现:

private T setInitialValue() {
	T value = initialValue();
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
	return value;
}

void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

该方法作用是将ThreadLocal对象存入当前线程对象的ThreadLocalMap中,最后返回变量初始值。
该方法首先会调用initialValue方法初始化变量,然后获取当前Thread对象所属的ThreadLocalMap(即成员变量threadLocals),如果Thread对象成员变量threadLocals不为空,则将当前ThreadLocal对象及其变量存入Map,否则会调用createMap方法为当前Thread对象新建一个ThreadLocalMap并存入当前ThreadLocal对象及其变量。

(3)ThreadLocal内部类:ThreadLocalMap

ThreadLocalMap负责存储ThreadLocal及其变量,即ThreadLocal对象本身作为键,ThreadLocal存储的变量作为值。每个Thread对象在声明一个ThreadLocal后会持有一个ThreadLocalMap的引用,来实现ThreadLocal的功能。

ThreadLocalMap持有一个内部类Entry,类似于HashMap.Node类,负责保存键值对。

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

Entry继承了WeakReference类,使Entry的键为弱引用。

看到这里很多人或许有这样一个疑问:为什么Entry要继承WeakReference?
既然将ThreadLocal声明为弱引用,那么自然会联想到和GC有关。

如果不声明为弱引用,以最上面Test类的代码为例,当我们将上述ThreadLocal类型的静态变量tl设置为null时,Thread对象成员变量threadLocals依然保留有一个ThreadLocalMap,该Map中持有保存该ThreadLocalEntry,在这个线程运行期间无法GC,从而引发内存泄漏。所以,Entry的键要声明为弱引用。
这里写图片描述
当我们弃用一个ThreadLocal对象时,最好通过调用其remove方法而不是直接设为null

当向ThreadLocalMap添加一个ThreadLocal时,会获取ThreadLocal对象的threadLocalHashCode获取HashCode添加至Map中而不是调用hashCode()方法。
ThreadLocalMap持有一个Entry数组,采用线性探测法处理哈希冲突,而不是HashMap的拉链法。
为了尽量避免哈希冲突,ThreadLocal提供了hashCode的计算代码:

private static AtomicInteger nextHashCode = new AtomicInteger();

/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;

/**
* Returns the next hash code.
*/
private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}

每当新建一个ThreadLocal对象,会调用nextHashCode获取自己的hashCode
关于魔数0x61c88647,有兴趣的可以百度,这里不再赘述。

(4)set方法分析
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}

get方法类似,set方法首先会获取当前运行的Thread对象并通过该对象获取对应的ThreadLocalMap,如果map为空,则为当前Thread对象新建一个ThreadLocalMap,否则直接将value放入map中。

(5)remove方法
public void remove() {
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null)
		m.remove(this);
}

同样,获取当前Thread对应的ThreadLocalMap,然后调用ThreadLocalMapremove方法移除ThreadLocal对象,无需通过弱引用机制对该ThreadLocal对象进行GC。

如果本文有错误或对文章有疑问,欢迎在评论区评论。

com.zd.archives.common.utils.ContextUtils.getDataSourceKey(ContextUtils.java:357) -http-nio-8080-exec-6-getDataSourceKey fail: java.lang.Exception: get dsKey is null at com.zd.archives.common.utils.ContextUtils.getDataSourceKey(ContextUtils.java:353) at com.zd.archives.sql.MultipleDataSource.determineCurrentLookupKey(MultipleDataSource.java:21) at org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource.determineTargetDataSource(AbstractRoutingDataSource.java:196) at org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource.getConnection(AbstractRoutingDataSource.java:164) at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:111) at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:77) at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:82) at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:68) at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:336) at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:84) at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62) at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324) at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)抛出异常了
03-21
<think>好的,我现在需要帮助用户解决他们遇到的异常问题。根据用户提供的日志信息,他们在调用`getDataSourceKey`方法时抛出了“get dsKey is null”的异常。首先,我得回顾一下之前对这个方法的分析,了解其工作原理和可能的问题点。 用户之前询问过`getDataSourceKey`方法的解释,现在遇到了实际运行中的异常。我需要结合之前的解析和当前的日志来分析原因。根据之前的讨论,该方法尝试从Session或ThreadLocal中获取数据源标识,如果两者都为空,就会抛出异常,最后回退到默认数据源。 现在日志显示在`http-nio-8080-exec-6`线程中,`getDataSourceKey`失败,原因是`dsKey`为null。这说明在尝试获取数据源键时,无论是Session还是ThreadLocal中都没有找到对应的值,导致抛出了异常。接下来,我需要考虑可能的原因和解决方案。 首先,可能的场景是,在Web请求处理中(如HTTP请求),代码试图从Session获取`dsKey`,但Session中没有设置该属性。或者,在非Web环境(如异步任务)中,ThreadLocal中的`dataSourceHolder`也没有正确设置。另外,可能是在某些情况下,虽然请求存在,但Session尚未创建,或者Session被意外清除。 接下来,我需要分析可能的原因,并列出排查步骤。例如,检查在请求处理流程中是否有正确设置`SESSION_DATA_SOURCE_KEY`到Session中,或者是否在非Web线程中正确使用`dataSourceHolder.set()`。此外,还要考虑是否有代码错误导致未正确设置,或者Session超时导致属性丢失。 另外,日志中的异常处理部分,虽然捕获了异常并记录了警告,但最终返回了默认数据源。这可能导致功能降级,但用户可能希望确保正确数据源被使用,而不是回退到默认。因此,需要确保在必要的地方正确设置数据源键,避免异常发生。 可能的解决方案包括:在用户登录或请求开始时设置数据源键到Session或ThreadLocal,检查设置逻辑是否正确,确保Session的有效性,或者在无法获取时提供更明确的错误信息。同时,可以优化代码,例如使用更安全的Session获取方式(`getSession(false)`)来避免自动创建Session,或者添加日志来追踪数据源键的设置和获取过程。 最后,需要将这些思路整理成用户容易理解的步骤,帮助他们逐步排查问题,并提供代码示例或配置建议,以便他们实施修复措施。</think>以下是针对该异常的详细分析和解决思路: --- ### **异常现象定位** 日志关键信息: ```java com.zd.archives.common.utils.ContextUtils.getDataSourceKey(ContextUtils.java:353) java.lang.Exception: get dsKey is null ``` - **问题本质**:系统在尝试获取动态数据源标识(`dsKey`)时,**Session 和 ThreadLocal 中均未找到有效值**。 - **触发场景**:发生在 `http-nio-8080-exec-6` 线程(Web 请求线程),说明是 HTTP 请求处理过程中未能正确设置或传递数据源标识。 --- ### **异常原因分析** #### 1. **Session 未设置数据源标识** - **典型场景**: - 用户登录后未调用 `request.getSession().setAttribute(GlobalConstant.SESSION_DATA_SOURCE_KEY, "your_ds_key")`。 - 会话超时导致 Session 失效,数据源标识丢失。 - **验证方法**: ```java // 在获取 dsKey 前添加调试日志 if (request != null && request.getSession() != null) { LOGGER.debug("Session中数据源标识: {}", request.getSession().getAttribute(GlobalConstant.SESSION_DATA_SOURCE_KEY)); } ``` #### 2. **ThreadLocal 未正确隔离** - **典型场景**: - 异步任务或非 Web 线程中未通过 `dataSourceHolder.set(dsKey)` 手动设置标识。 - 线程复用导致 ThreadLocal 残留脏数据(需检查 `dataSourceHolder` 是否在 finally 块中清理)。 - **验证方法**: ```java // 在 else 分支中添加日志 else { dsKey = dataSourceHolder.get(); LOGGER.debug("ThreadLocal中数据源标识: {}", dsKey); } ``` #### 3. **防御性代码缺陷** - `request.getSession()` 可能隐式创建新 Session(默认行为为 `true`),但新 Session 中无数据源标识。 - **改进建议**:改用 `request.getSession(false)` 避免自动创建空 Session。 --- ### **解决方案** #### 1. **确保数据源标识设置** - **Web 请求场景**:在用户登录或路由拦截器中显式设置 Session 属性。 ```java @PostMapping("/login") public void login(HttpServletRequest request, String tenantId) { request.getSession().setAttribute(GlobalConstant.SESSION_DATA_SOURCE_KEY, tenantId); } ``` - **非 Web 场景**:在异步任务入口处通过 ThreadLocal 设置。 ```java public void asyncTask(String dsKey) { try { dataSourceHolder.set(dsKey); // 执行业务逻辑... } finally { dataSourceHolder.remove(); // 必须清理避免内存泄漏 } } ``` #### 2. **优化防御性校验逻辑** ```java // 修改 Session 获取逻辑,避免自动创建 HttpSession session = request != null ? request.getSession(false) : null; if (session != null) { dsKey = session.getAttribute(GlobalConstant.SESSION_DATA_SOURCE_KEY); } else { dsKey = dataSourceHolder.get(); } ``` #### 3. **增强异常信息可读性** - 抛出明确异常类型,便于问题定位: ```java if (dsKey == null) { throw new DataSourceKeyNotFoundException("数据源标识缺失,请检查 Session 或 ThreadLocal 设置"); } ``` --- ### **排查流程图** ```mermaid graph TD A[异常: get dsKey is null] --> B{是否为 Web 请求?} B -->|是| C[检查 Session 中 SESSION_DATA_SOURCE_KEY 是否设置] B -->|否| D[检查 ThreadLocal 中 dataSourceHolder 是否赋值] C --> E[验证登录/拦截器逻辑是否正确设置] D --> F[确认异步任务是否调用 dataSourceHolder.set()] E --> G[检查 Session 超时时间配置] F --> H[确保 finally 块清理 ThreadLocal] ``` --- ### **关键日志补充建议** 在以下位置添加调试日志,辅助问题追踪: ```java // ContextUtils.java 中 getDataSourceKey() 方法 LOGGER.debug("当前线程: {}, 数据源标识来源: {}", Thread.currentThread().getName(), (session != null) ? "Session" : "ThreadLocal"); ``` --- 通过上述分析和调整,可系统性解决数据源标识丢失问题,确保动态路由机制稳定运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值