ThreadLocal 全面解析
目录
1. ThreadLocal 介绍
1.1 官方介绍
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* <p>Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
public class ThreadLocal<T> {
...
}
此类提供线程 thread-local 局部变量。这些变量与普通对应变量的不同之处在于,访问一个变量的每个线程(通过其 {@code get} 或 {@code set} 方法)都有自己的独立初始化的变量副本。{@code ThreadLocal} 实例通常是希望将状态与线程(例如,用户 ID 或事务 ID)相关联的类中的私有静态字段。
<p>例如,下面的类生成每个线程的本地唯一标识符。
线程的 id 在第一次调用 {@code ThreadId.get()} 时分配,并在后续调用中保持不变。
<pre>
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer 包含下一个要分配的线程 ID
private static final AtomicInteger nextId = new AtomicInteger(0);
// 线程局部变量,包含每个线程的 ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// 返回当前线程的唯一 ID,必要时分配它
public static int get() {
return threadId.get();
}
}
<pre>
<p>
每个线程都保留对其线程局部变量副本的隐式引用,只要线程处于活动状态并且 {@code ThreadLocal} 实例可访问;线程消失后,其线程本地实例的所有副本都将受到垃圾回收(除非存在对这些副本的其他引用)。
@author Josh Bloch 和 Doug Lea
@since 1.2
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其它线程内的变量。ThreadLocal实例通常来说都是 private static 类型的,用于关联线程和线程上下文。
我们可以得知TheadLocal的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
总结:
1. 线程并发:在多线程并发的场景下(单线程是用不到 ThreadLocal的)
2. 传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3. 线程隔离:每个线程的 ThreadLocal 变量都是独立的,不会互相影响(核心)
1.2 基本使用
1.2.1 常用方法
在使用之前,我们先来认识几个 ThreadLocal 的常用方法:
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
1.2.2 使用案例
我们来看下面这个案例,感受一下 ThreadLocal 线程隔离的特点:
package org.thread_local;
/**
* 需求:线程隔离
* 在多线程并发的场景下,每个线程中的变量都是相互独立的
* 线程 A:设置(变量 1) 获取(变量 1)
* 线程 B:设置(变量 2) 获取(变量 2)
*
* ThreadLocal:
* 1. set():将变量绑定到当前线程中
* 2. get():获取当前线程绑定的变量
*/
public class MyDemo01 {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 变量
private String content;
private String getContent() {
// return content;
// 获取当前线程绑定的变量
return threadLocal.get();
}
private void setContent(String content) {
// this.content = content;
// 变量 content 绑定到当前线程
threadLocal.set(content);
}
public static void main(String[] args) {
MyDemo01 demo = new MyDemo01();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
/**
* 每个线程:存一个变量,过一会 取出这个变量
*/
demo.setContent(Thread.currentThread().getName() + " 的数据");
System.out.println("----------------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
});
thread.setName("线程" + i);
thread.start();
}
System.out.println(demo.getContent());
}
}
1.3 ThreadLocal 类与 synchronized 关键字
1.3.1 synchronized 同步方式
这里可能有的朋友会觉得在上述例子中我们完全可以通过加锁来实现这个功能,我们首先来看一下用 synchronized 代码块实现的效果(使用synchronized解决线程隔离问题):
package org.thread_local;
public class MyDemo02 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
MyDemo02 demo02 = new MyDemo02();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (MyDemo02.class) {
demo02.setContent(Thread.currentThread().getName() + " 的数据");
System.out.println("----------------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo02.getContent());
}
}, "线程" + i).start();
}
System.out.println(demo02.getContent());
}
}
从执行结果可以发现,加锁确实可以解决线程隔离问题,但是在这里我们强调的是线程数据隔离的问题,并不是多线程共享数据的问题,在这个案例中使用 synchronized 关键字是不合适的。
1.3.2 ThreadLocal 与 synchronized 的区别
虽然 ThreadLocal 模式与 synchronized 关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。侧重点不同,解决的问题也不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用 ’以时间换空间‘ 的方式。只提供了一份变量,让不同的线程排队访问 | ThreadLocal 采用 ’以空间换时间‘ 的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多个线程中每个线程之间的数据相互隔离 |
总结:在上面的案例中,虽然使用 ThreadLocal 和 synchronized 都能解决问题,但是使用 ThreadLocal 更为合适,因为这样可以使程序拥有更高的并发性。
2. 运用场景_事务案例
通过以上的介绍,我们已经基本了解了 ThreadLocal 的特点。但是它具体是运用在什么场景中呢?接下来让我们看一个案例:事务操作。
2.1 转账案例
2.1.1 场景构建
这里我们先构建一个简单的转账场景:有一个数据表 account,里面有两个用户 Jack 和 Rose,用户 Jack 给用户 Rose 转账。
案例的实现主要用 mysql 数据库,JDBC 和 C3P0 框架。以下是详细代码:
(1)项目结构
pom.xml
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>mchange-commons-java</artifactId>
<version>0.2.12</version>
</dependency>
</dependencies>
(2)c3p0-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<!--默认配置。详细配置可查阅在线文档:https://tool.oschina.net/apidocs/apidoc?api=c3p0 -->
<default-config>
<property name="initialPoolSize">10</property>
<property name="maxIdleTime">30</property>
<property name="maxPoolSize">100</property>
<property name="minPoolSize">10</property>
<property name="maxStatements">200</property>
</default-config>
<!--配置连接池mysql-->
<named-config name="mysql">
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf8</property>
<property name="user">root</property>
<property name="password">root</property>
<!--连接池参数-->
<property name="initialPoolSize">10</property>
<property name="maxIdleTime">30</property>
<property name="maxPoolSize">100</property>
<property name="minPoolSize">10</property>
<property name="maxStatements">200</property>
<property name="checkoutTimeout">3000</property>
</named-config>
</c3p0-config>
(3)log4j.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
<log4j:configuration>
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<param name="Encoding" value="UTF-8"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n"/>
</layout>
</appender>
<logger name="java.sql">
<level value="debug"/>
</logger>
<logger name="org.apache.ibatis">
<level value="info"/>
</logger>
<root>
<level value="debug"/>
<appender-ref ref="STDOUT"/>
</root>
</log4j:configuration>
(4)JdbcUtil
package org.thread_local.util;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class JdbcUtil {
// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource("mysql");
// 获取连接
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
// 释放资源
public static void release(AutoCloseable... ios) {
for (AutoCloseable io : ios) {
if (io != null) {
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void commitAndClose(Connection conn) {
try {
if (null != conn) {
// 提交事务
conn.commit();
// 释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose(Connection conn) {
try {
if (null != conn) {
// 回滚事务
conn.rollback();
// 释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
(5)AccountDao
package org.thread_local.dao;
import org.thread_local.util.JdbcUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AccountDao {
// 转出
public void out(String outUser, int money) throws SQLException {
String sql = "update t_account set money = money - ? where name = ?";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ps.setObject(1, money);
ps.setObject(2, outUser);
ps.executeUpdate();
JdbcUtil.release(connection, ps);
}
// 转入
public void in(String inUser, int money) throws SQLException {
String sql = "update t_account set money = money + ? where name = ?";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ps.setObject(1, money);
ps.setObject(2, inUser);
ps.executeUpdate();
JdbcUtil.release(connection, ps);
}
}
(6)AccountService
package org.thread_local.service;
import org.thread_local.dao.AccountDao;
public class AccountService {
// 转账
public boolean transfer(String outUser, String inUser, int money) {
AccountDao accountDao = new AccountDao();
try {
// 转出
accountDao.out(outUser, money);
// 转入
accountDao.in(inUser, money);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
(7)AccountWeb
package org.thread_local.web;
import org.thread_local.service.AccountService;
public class AccountWeb {
public static void main(String[] args) {
// 模拟数据:Jack 给 Rose 转账 100
String outUser = "Jack";
String inUser = "Rose";
int money = 100;
AccountService accountService = new AccountService();
boolean result = accountService.transfer(outUser, inUser, money);
if (result) {
System.out.println("转账成功!");
} else {
System.out.println("转账失败!");
}
}
}
2.1.2 引入事务
案例中的转账涉及两个 DML 操作:一个转出,一个转入。这些操作是需要具备原子性的,不可分割。不然就有可能出现数据修改异常情况。
package org.thread_local.service;
import org.thread_local.dao.AccountDao;
public class AccountService {
// 转账
public boolean transfer(String outUser, String inUser, int money) {
AccountDao accountDao = new AccountDao();
try {
// 转出
accountDao.out(outUser, money);
// 算术异常:模拟转出成功,转入失败的异常
int i = 1 / 0;
// 转入
accountDao.in(inUser, money);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
所以这里就需要操作事务,来保证转出和转入操作具备原子性,要么同时成功,要么同时失败。
1)JDBC 中关于事务操作的 api
Connection 接口的方法 | 作用 |
---|---|
void setAutoCommit(false); | 禁用事务自动提交(手动) |
void commit(); | 提交事务 |
void rollback(); | 回滚事务 |
2)开启事务的注意点:
- 为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个:service 层开启事务的 connection 需要跟 dao 层访问数据库的 connection 保持一致
- 线程并发情况下,每个线程只能操作各自的 connection(每个线程的 connection 必须线程隔离)
2.2 常规解决方案
2.2.1 常规方案的实现
基于上面给出的前提,大家通常想到的解决方案是:
- 从 service 层将 connection 对象向 dao 层传递
- 加锁
以下是代码实现修改的部分:
(1)AccountService 类
/**
* 事务的使用注意点:
* 1. service 层 和 dao 层 的连接对象保持一致
* 2. 每个线程的 connection 对象必须前后一致,线程隔离
*
* 常规解决方案:
* 1. 传参:将 service 层的 connection 对象直接传递到 dao 层
* 2.加锁
*/
public class AccountService {
// 转账
public boolean transfer(String outUser, String inUser, int money) {
AccountDao accountDao = new AccountDao();
Connection conn = null;
try {
synchronized (AccountService.class) {
// 1. 开启事务
conn = JdbcUtil.getConnection();
conn.setAutoCommit(false);
// 转出
accountDao.out(outUser, money, conn);
// 算术异常:模拟转出成功,转入失败的异常
int i = 1 / 0;
// 转入
accountDao.in(inUser, money, conn);
// 2. 成功提交
JdbcUtil.commitAndClose(conn);
}
} catch (Exception e) {
e.printStackTrace();
// 2. 或者失败回滚
JdbcUtil.rollbackAndClose(conn);
return false;
}
return true;
}
}
(2)AccountDao 类
/**
* 常规方案:
* 1.方法添加一个参数 connection
* 2.不能从连接池中获取连接,直接使用参数 connection
* 3.注意:dao 层不能释放连接
*/
public class AccountDao {
// 转出
public void out(String outUser, int money, Connection connection) throws SQLException {
String sql = "update t_account set money = money - ? where name = ?";
// Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ps.setObject(1, money);
ps.setObject(2, outUser);
ps.executeUpdate();
// JdbcUtil.release(connection, ps);
}
// 转入
public void in(String inUser, int money, Connection connection) throws SQLException {
String sql = "update t_account set money = money + ? where name = ?";
// Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ps.setObject(1, money);
ps.setObject(2, inUser);
ps.executeUpdate();
// JdbcUtil.release(connection, ps);
}
}
2.3 ThreadLocal 解决方案
2.3.1 ThreadLocal 方案的实现
(1)JdbcUtil
public class JdbcUtil {
private static final ThreadLocal<Connection> local = new ThreadLocal<>();
// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource("mysql");
// 获取连接
/**
* 原本:直接从连接池中获取连接
* 改造:
* 1.直接获取当前线程绑定的对象
* 2.如果连接对象是空的
* 2.1 再去连接池中获取连接
* 2.2 将此连接对象跟当前线程进行绑定
*/
public static Connection getConnection() throws SQLException {
if (local.get() == null) {
local.set(ds.getConnection());
}
return local.get();
}
// 释放资源
public static void release(AutoCloseable... ios) {
for (AutoCloseable io : ios) {
if (io != null) {
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void commitAndClose(Connection conn) {
try {
if (null != conn) {
// 提交事务
conn.commit();
// 解绑当前线程绑定的连接对象
local.remove();
// 释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose(Connection conn) {
try {
if (null != conn) {
// 回滚事务
conn.rollback();
// 解绑当前线程绑定的连接对象
local.remove();
// 释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
(2)AccountService
/**
* 事务的使用注意点:
* 1. service 层 和 dao 层 的连接对象保持一致
* 2. 每个线程的 connection 对象必须前后一致,线程隔离
*
* 常规解决方案:
* 1. 传参:将 service 层的 connection 对象直接传递到 dao 层
* 2.加锁
*
* 常规解决方案的弊端:
* 1.提高代码耦合度
* 2.降低程序性能
*/
public class AccountService {
// 转账
public boolean transfer(String outUser, String inUser, int money) {
AccountDao accountDao = new AccountDao();
Connection conn = null;
try {
// synchronized (AccountService.class) {
// 1. 开启事务
conn = JdbcUtil.getConnection();
conn.setAutoCommit(false);
// 转出
accountDao.out(outUser, money);
// 算术异常:模拟转出成功,转入失败的异常
int i = 1 / 0;
// 转入
accountDao.in(inUser, money);
// 2. 成功提交
JdbcUtil.commitAndClose(conn);
// }
} catch (Exception e) {
e.printStackTrace();
// 2. 或者失败回滚
JdbcUtil.rollbackAndClose(conn);
return false;
}
return true;
}
}
(3)AccountDao
/**
* 常规方案:
* 1.方法添加一个参数 connection
* 2.不能从连接池中获取连接,直接使用参数 connection
* 3.注意:dao 层不能释放连接
*/
public class AccountDao {
// 转出
public void out(String outUser, int money) throws SQLException {
String sql = "update t_account set money = money - ? where name = ?";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ps.setObject(1, money);
ps.setObject(2, outUser);
ps.executeUpdate();
// JdbcUtil.release(connection, ps);
}
// 转入
public void in(String inUser, int money) throws SQLException {
String sql = "update t_account set money = money + ? where name = ?";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ps.setObject(1, money);
ps.setObject(2, inUser);
ps.executeUpdate();
// JdbcUtil.release(connection, ps);
}
}
2.3.2 ThreadLocal 方案的好处
从上述的案例中我们可以看到,在一些特定场景下,ThreadLocal 方案有两个突出的优势:
- 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
- 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
3. ThreadLocal 的内部结构
通过以上的学习,我们对 ThreadLocal 的作用有了一定的认识。现在我们一起来看一下 ThreadLocal 的内部结构,探究它能够实现线程数据隔离的原理。
3.1 常见的误解
通常,如果我们不去看源代码的话,我猜 ThreadLocal
是这样子设计的:每个 ThreadLocal
类都创建一个 Map
,然后用线程的 ID threadID
作为 Map
的 key
,要存储的局部变量作为 Map
的 Value
,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal
就是这样设计的。
3.2 核心结构
但是,JDK后面优化了设计方案,现在JDK8ThreadLocal
的设计是:每个Thread
维护一个ThreadLocalMap
哈希表,这个哈希表的key
是 ThreadLocal
实例本身,value
才是真正要存储的值object
。
- 每个Thread线程内部都有一个Map(ThreadLocalMap)
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰(局部变量,线程之间相互隔离)
3.2 JDK8的设计方案有两个好处
- 每个 Map 存储的 Entry 数量变少:JDK8以前,ThreadLocalMap Entry 的数量是与 Thread 成正比的;JDK8之后 ThreadLocalMap Entry 的数量与 ThreadLocal 成正比,ThreadLocal 的数量在实际开发中是远少于 Thread 的数量的,因此,每个 Map 存储的 Entry 数量就变少了,然后就尽可能的避免了 hash 冲突的问题。
- 当 Thread 销毁的时候,ThreadLocalMap 也会随之销毁,减少内存的使用/开销。
4. ThreadLocal 的核心方法源码
基于 ThreadLocal 的内部结构,我们继续分析它们的核心方法源码,更深入的了解其操作原理。除了构造方法之外,ThreadLocal 对外暴露的方法有以下 4 个:
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
以下是这 4 个方法的详细源码分析(为了保证思路清晰,ThreadLocalMap 部分暂时不展开,下一个知识点详解)
4.1 set 方法
(1)源码及对应的中文注释
/**
* 设置当前县城对应的 ThreadLocal 的值
*
* @param value 将要保存在当前线程对应的 ThradLocal 的值
*/
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取此线程对象中维护的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 判断 map 是否存在
if (map != null)
// 存在则调用 map.set 设置此实体 entry
map.set(this, value);
else
// 1)当前线程 Thrad 不存在 ThreadLocalMap 对象
// 2)则调用 createMap 进行 ThreadLocalMap 对象的初始化
// 3)并将 t(当前线程)和 value(t 对应的值)作为第一个 entry 存放至 ThradLocalMap 中
createMap(t, value);
}
/**
* 获取当前线程 Thread 对应维护的 ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的 ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建当前线程 Thread 对应维护的 ThreadLocalMap
*
* @param t the current thread 当前线程
* @param firstValue value for the initial entry of the map 存放到 map 中第一个 entry 的值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
(2)代码执行流程
- 首先获取当前线程,并根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则将参数设置到 Map 中(当前 ThreadLocal 的引用作为 key)
- 如果 Map 为空,则给该线程创建 Map,并设置初始值
4.2 get 方法
(1)源码及对应的中文注释
/**
* 返回当前线程种保存 ThreadLocal 的值
* 如果当前线程没有此 ThreadLocal 变量,
* 则它会通过调用 {@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此 ThreadLocal 的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 如果此 map 存在
if (map != null) {
// 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对 e 进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value 值
// 即为我们想要的当前线程对应此 ThreadLocal 的值
T result = (T)e.value;
return result;
}
}
/*
初始化 :有两种情况要执行当前代码
第一种情况 :map 不存在,表示此线程没有维护的 ThreadLocalMap 对象
第二种情况 :map 存在,但是没有与当前 ThreadLocal 关联的 Entry
*/
return setInitialValue();
}
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用 initialValue 获取初始化的值
// 此方法可以被子类重写,如果不重写默认返回 null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 判断 map 是否存在
if (map != null)
// 存在则调用 map.set 设置此实体 entry
map.set(this, value);
else
// 1)当前线程 Thread 不存在 ThreadLocalMap 对象
// 2)则调用 createMap 进行 ThreadLocalMap 对象的初始化
// 3)并将 t(当前线程)和 value(对应的值)作为第一个 enyru 存放至 ThreadLocalMap 中
createMap(t, value);
// 返回设置的值 value
return value;
}
/**
* 获取与密钥关联的条目。此方法本身仅处理快速路径:直接命中现有键。否则,它会中继到 getEntryAfterMiss。
* 这旨在最大限度地提高直接命中的性能,部分原因是使此方法易于内联。
* 参数:
* key – 线程本地对象
* 返回:与键关联的条目,如果没有这样的条目,则为 null
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
(2)代码执行流程
- 首先获取当前线程,根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则在 Map 中以 ThreadLocal 的引用作为 key 来在 Map中获取对应的 Entry e,否则转到 4
- 如果 e 不为 null,则返回 e.value,否则转到 4
- Map 为空或者 e 为空,则通过 initialValue 函数获取初始值 value,然后用 ThreadLocal 的引用和 value 作为 firstKey 和 firstValue 创建一个新得 Map
总结:先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。
4.3 remove 方法
(1)源码及对应的中文注释
/**
* 删除当前线程中保存的 ThreadLocal 对应的实体 Entry
*/
public void remove() {
// 获取当前线程中维护的 ThreadLocalMap 对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此 map 存在
if (m != null)
// 存在则调用 map.remove
// 以当前 ThreadLocal 为 key 删除对应的实体 Entry
m.remove(this);
}
(2)代码执行流程
- 首先会获取当前线程,并根据当前线程获取一个 Map
- 如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 Entry
4.4 initialValue 方法
/**
* 返回当前线程对应的 ThreadLocal 的初始值
*
* 此方法第一次调用发生在,当前线程通过 get 方法访问此线程的 ThreadLocal 值时
* 除非线程先调用了 set 方法,在这种情况下,initialValue 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* <p>这个方法仅仅简单的返回 null {@code null};
* 如果程序员想 ThreadLocal 线程局部变量有一个除 null 以外的初始值,
* 必须通过子类继承 {@code ThreadLocal} 的方式去重写此方法
* 通常,可以通过匿名内部类的方式实现
*
* @return the initial value for this thread-local 当前 ThreadLocal 的初始值
*/
protected T initialValue() {
return null;
}
此方法的作用是 返回该线程局部变量的初始值
- 这个方法是一个延迟调用方法,从上面的代码我们得知,在 set 方法还未调用而先调用了 get 方法时才执行,并且仅执行 1 次。
- 这个方法缺省/默认实现直接返回一个 null
- 如果想要一个除 null 之外的初始值,可以重写此方法(备注:该方法时一个 protected 的方法,显然为了让子类覆盖而设计的)
5.ThreadLocalMap 源码分析
在分析 ThreadLocal 方法的时候,我们了解到 ThreadLocal 的操作实际上是围绕 ThreadLocalMap 展开的。ThreadLocalMap 的源码相对比较复杂,我们从以下三个方面进行讨论。
5.1 基本结构
ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也是独立实现。
ThreadLocalMap 的三个方法在 ThreadLocal 的核心方法中都有映射,即都使用过
WeakReference:弱引用
(1) 成员变量
/**
* The initial capacity -- MUST be a power of two.
* 初始容量——必须是 2 的整数次幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* 存放数据的 table,Entry 类的定义在下面分析
* 统一,数组的长度必须是 2 的整次幂。
* 核心容器:用于存 Entry 键值对的数组,Map 其实就是键值对的集合
*/
private Entry[] table;
/**
* The number of entries in the table.
* 数组里面 entry 的个数,可以用于判断 table 当前使用量是否超过阈值。
*/
private int size = 0;
/**
* The next size value at which to resize.
* 进行扩容的阈值,表示使用量大于它的时候进行扩容。
*/
private int threshold; // Default to 0
跟 HashMap 类似,INITIAL_CAPACITY 代表这个 Map 的初始容量;table 是一个 Entry 类型的数组,用于存储数据;size 代表表中的存储数目;threshold 代表需要扩容时对应 size 的阈值。
(2) 存储结构 - Entry
/**
* Entry 继承 WeakReference(弱引用),并且用 ThreadLocal 作为 key.
* 如果 key 为 null(entry.get() == null),意味着 key 不在被引用
* 因此这时候 entry 也可以从 table 中清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在 ThreadLocalMap 中,也是用 Entry 来保存 K-V 结构数据的。不过 Entry 中的 key 只能是 ThreadLocal 对象,这点在构造方法中已经限定死了。
另外,Entry 继承 WeakReference,也就是 key(ThreadLocal)是弱引用,其目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑。
5.2 弱引用和内存泄漏
有些程序员在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟 Entry 中使用了弱引用的 key 有关系。这个理解其实是不对的。
我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。
(1) 内存泄漏相关概念
- Memory overflow:内存溢出,没有足够的内存提供申请者使用。
- Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
(2) 弱引用相关概念
Java中的引用有 4 种类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
强引用(”strong“ Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还”活着“,垃圾回收器就不会回收这种对象。就算内存溢出,内存不够了,它宁愿抛出异常也不会回收这些强引用。
弱引用(WeakReference),垃圾回收期一旦发现了只具有弱引用的对象,不管当前内存足够与否,都会回收它的内存。如果弱引用对象被其它强引用对象所引用,那么它依然不会被垃圾回收器所回收掉。
(3) 如果 key 使用强引用
假设 ThreadLocalMap 中的 key 使用了强引用,那么会出现内存泄漏吗?
此时 ThreadLocal 的内存圈(实现表示强引用)如下:
(4) 如果 key 使用弱引用
在使用完 ThreadLocal 之后要从 ThreadLocalMap 中 remove Entry(threadLocal.remove() 即可删除 ThreadLocalMap 中的 Entry),否则 key 为 null 了,但是 value 还是有值的,这块 value 由于没有对应的 key 是永远不会被访问到的,不会被用到又不会被回收就会导致 value 内存泄漏。
(5) 出现内存泄漏的真实原因
(6) 为什么使用弱引用
根据上述分析,我们知道了:无论使用 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
- 使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry
- 使用完 ThreadLocal,当前 Thread 也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
也就是说,只要记得在使用完 ThreadLocal 后及时调用 remove。无论 key 是强引用还是弱引用都不会有问题。
那么为什么 key 要使用弱引用呢?
事实上,再 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 为 null(也就是 ThreadLocal 为 null)进行判断,如果为 null 的话,那么是会对 value 置为 null 的。
这就意味着使用完 ThreadLocal,CurrentThread 依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 中的任一方法的时候会被清除,从而避免内存泄漏。(仍然是不安全的)
如果想彻底避免内存泄漏,还是得在使用完 ThreadLocal 之后调用它的 remove 方法。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
5.3 hash 冲突的解决
hash 冲突的解决是 Map 中的一个重要内容,我们以 hash 冲突的解决为线索,来研究一下 ThreadLocalMap 的核心源码。
(1) 首先从 ThreadLocal 的 set() 方法入手
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 调用 ThreadLocalMap 的 set 方法
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
// 调用了 ThreadLocalMap 的构造方法
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这个方法我们上面以及分析过了,其作用是设置当前线程绑定的局部变量:
-
首先获取当前线程,并根据当前线程获取一个 Map
-
如果获取的 Map 不为空,则将参数设置到 Map 中(当前 ThreadLocal 的引用为 key)
(这里调用了 ThreadLocalMap 的 set 方法)
-
如果 Map 为空,则给该线程创建 Map ,并设置初始值
(这里调用了 ThreadLocalMap 的构造方法)
这段代码有两个地方分别涉及到 ThreadLocalMap 的两个方法,我们接着分析这两个方法。
(2) 构造方法 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
/**
* firstKey:本 ThreadLocal 实例(this)
* firstValue:要保存的线程本地变量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 table 16
table = new Entry[INITIAL_CAPACITY];
// 计算索引(重点代码):减少hash冲突
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 设置值
table[i] = new Entry(firstKey, firstValue);
size = 1; // 数组中存储 Entry 的个数
// 设置阈值,是初始容量的 2/3
setThreshold(INITIAL_CAPACITY);
}
构造函数首先创建一个长度为 16 的 Entry 数组,然后计算出 firstKey 对应的索引,然后存储到 table 中,并设置 size 和 threshold。
重点分析:int i = firstKey.threadLocalHashCode & (INTIAL_CAPACITY - 1)
。
a. 关于 firstKey.threadLocalHashCode
:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// AtomicInteger 是一个提供原子操作的 Integer 类,通过线程安全的方式操作加减,适合高并发情况下的使用
private static AtomicInteger nextHashCode = new AtomicInteger();
// 特殊的 hash 值
private static final int HASH_INCREMENT = 0x61c88647;
这里定义了一个 AtomicInteger 类型,每次获取当前值并加上 HASH_INCREMENT,HASH_INCREMENT = 0x61c88647
,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在 2 的 n 次方的数组里,也就是 Entry[] table 中,这样做可以尽量避免 hash 冲突。
b. 关于 & (INITIAL_CAPACITY - 1)
计算 hash 的时候里面采用了 hashCode & (size - 1) 的算法,这相当于取模运算 hashCode % size 的一个更高效的实现,正是因为这种算法,我们要求 size 必须是 2 的整数次幂,这也能保证在索引不越界的前提下,使得 hash 发生冲突的次数减小。
(3) ThreadLocalMap 中的 set 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算索引(重点代码,刚才分析过)
int i = key.threadLocalHashCode & (len-1);
/**
* 使用线性探测法查找元素(重点代码)
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// ThreadLocal 对应的 key 存在,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
// key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
// 当前数组中的 Entry 是一个陈旧(stale)的元素
if (k == null) {
// 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal 对应的 key 不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots 用于清除那些 e.get() == null 的元素,
* 这种数据 key 关联的对象已经被回收,所以这个 Entry(table[index]) 可以被置为 null.
* 如果没有清除任何 entry,并且当前使用量达到了负载因子所定义(长度的 2/3),那么进行
* rehash(执行一次全表的扫描清理工作)
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 获取环形数组的下一个索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
代码执行流程:
- 首先还是根据key计算出索引i,然后查找i位置上的Entry,
- 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
- 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
- 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。
重点分析 :ThreadLocalMap使用 线性探测法
来解决哈希冲突的
- 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
- 举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如table[14]上已经有值并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
- 按照上面的描述,可以把 Entry[] table 看成一个环形数组
null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
// 当前数组中的 Entry 是一个陈旧(stale)的元素
if (k == null) {
// 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal 对应的 key 不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots 用于清除那些 e.get() == null 的元素,
* 这种数据 key 关联的对象已经被回收,所以这个 Entry(table[index]) 可以被置为 null.
* 如果没有清除任何 entry,并且当前使用量达到了负载因子所定义(长度的 2/3),那么进行
* rehash(执行一次全表的扫描清理工作)
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
- 获取环形数组的下一个索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
代码执行流程:
1. 首先还是根据key计算出索引i,然后查找i位置上的Entry,
2. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
3. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
4. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。
**重点分析** :ThreadLocalMap使用 `线性探测法` 来解决哈希冲突的
1. 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
2. 举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如table[14]上已经有值并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
3. 按照上面的描述,可以把 Entry[] table 看成一个环形数组