【JUC剖析】ThreadLocal类 详解
我们在阅读 Mybatis框架源码 时,会看到一个 ThreadLocal类
而这个类,常常作为面试考点,
可以看出:
我们若是想要 了解底层实现,学习高妙的编程思维,就不得不来学习 ThreadLocal类
那么,在本篇博文中,本人就来讲解下 ThreadLocal类:
基本概念:
官方简介:
此类提供 线程局部变量
这些变量与普通变量不同,每个线程(通过其get或set方法)的线程都有其 自己的、独立初始化的 变量副本
ThreadLocal实例 通常是希望将 状态 与 线程 关联的类 中的 私有静态字段
(例如,用户ID或交易ID)
例如,下面的类生成每个线程本地的唯一标识符。
线程的ID是在第一次调用ThreadId.get()时分配的,并且在后续调用中保持不变
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();
}
}
只要线程是 active 并且 ThreadLocal实例是可访问的,则 每个线程 都对其 线程局部变量的副本 持有
隐式引用
线程 失活 后,其线程 本地实例的所有副本 都将进行 垃圾回收
(除非存在对这些副本的其他引用)
从 JDK官方文档 中的描述:
ThreadLocal类 用来 提供 线程内部的局部变量
这种变量在 多线程环境 下访问(通过 get和set方法 访问)时能保证 各个线程的变量 相对 独立 于 其他线程内的变量
ThreadLocal实例 通常来说都是 private static权限 的,用于关联 线程 和 线程上下文
我们可以得知 ThreadLocal 的 作用 是:
提供线程内的 局部变量,不同的线程 之间 不会相互干扰
这种变量在 线程的生命周期内 起作用,
减少 同一个线程 内 多个函数或组件 之间一些 公共变量 的 传递复杂度
那么,本人来总结下 官方介绍 中的 重要内容:
总结:
线程并发
:
在 多线程并发 的场景下传递数据
:
我们可以通过ThreadLocal在 同一线程,不同组件 中传递 公共变量线程隔离
:
每个线程的变量都是 独立 的,不会相互影响
首先,我们来认识几个ThreadLocal的常用方法:
常用方法:
方法声明 | 描述 |
---|---|
new ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置 当前线程绑定的局部变量 |
public T get() | 获取 当前线程绑定的局部变量 |
public void remove() | 移除 当前线程绑定的局部变量 |
那么,本人现在来通过一个案例来展示下 ThreadLocal类 的 使用:
案例:
首先,我们来故意制造一个有 线程安全问题 的代码:
问题出现:
package edu.youzg.threadlocal;
public class MyDemo {
private String content;
private String getContent() {
return content;
}
private void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
MyDemo demo = new MyDemo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "->" + demo.getContent());
}, "线程" + i).start();
}
}
}
那么,现在本人来展示下 运行结果:
从上面的运行结果,我们可以看出:
多个线程 在访问 同一变量 的时候出现的异常,线程间的数据 没有隔离。
下面我们来看下采用 ThreadLocal 的方式来解决这个例子的问题:
问题解决:
package edu.youzg.threadlocal;
public class MyDemo {
private ThreadLocal<String> content = new ThreadLocal<>();
private String getContent() {
return content.get();
}
private void setContent(String content) {
this.content.set(content);
}
public static void main(String[] args) {
MyDemo demo = new MyDemo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "->" + demo.getContent());
}, "线程" + i).start();
}
}
}
本人来展示下 运行结果:
从上面的使用案例中,我们能够看出:
ThreadLocal 很好地 解决了 多线程之间数据隔离 问题!
谈及 并发
和 同步
,我们就不得不来讲解下 synchronized关键字
:
ThreadLocal 与 synchronized:
可能会有同学会觉得在上述例子中,我们完全可以通过加锁来实现这个功能。
那么,本人首先来用 synchronized关键字,来解决上述 线程安全问题:
synchronized 解决方案:
package edu.youzg.threadlocal;
public class MyDemo {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
MyDemo demo = new MyDemo();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (MyDemo.class) {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "->" + demo.getContent());
}
}, "线程" + i).start();
}
}
}
运行结果 如下:
从上述运行结果,可以发现:
加锁确实可以解决这个问题,
但是在这里我们强调的是 线程数据隔离问题,
并 不是 多线程共享数据的问题,
因此,在这个案例中使用synchronized关键字
是不合适的,有点 “杀鸡用牛刀” 的感觉... ...
那么,根据上述例子,本人来总结下 两者的区别:
区别:
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用 “以时间换空间” 的方式 只提供了一份变量,让不同的线程排队访问 |
ThreadLocal采用“以空间换时间”的方式 为 每个线程 都提供了 一份变量的副本,从而实现 同时访问 而 相不干扰 |
侧重点 | 多线程之间 的 同步性 |
多线程之间 的 数据相互隔离 |
那么,依据上文原理,本人来总结下 ThreadLocal和synchronized:
总结:
虽然使用 ThreadLocal 和 synchronized 都能解决问题,
但是使用 ThreadLocal 更为合适
因为这样可以使程序拥有 更高的并发性
相信认真看完了上文的讲解,同学们都大致明白了 ThreadLocal的 概念及用法
那么,在讲解完 “是什么
” 问题后,本人来讲解下 代表 “怎么做
” 的 运用场景 问题:
适用场景:
说明:
关于
ThreadLocal类
,有一个非常重要的 使用场景 ——事务
:
案例:
场景构建:
这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。
案例的实现就简单的用mysql数据库,JDBC 和 C3P0 框架实现。以下是详细代码 :
(1) 项目结构
(2) 数据准备
-- 使用数据库
use test;
-- 创建一张账户表
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
-- 初始化数据
insert into account values(null, 'Jack', 1000);
insert into account values(null, 'Rose', 1000);
(3) C3P0配置文件和工具类
<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
<!-- 连接参数 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
<property name="user">root</property>
<property name="password">1234</property>
<!-- 连接池参数 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">3000</property>
</default-config>
</c3p0-config>
(4) 工具类 : JdbcUtils
package com.itheima.transfer.utils;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class JdbcUtils {
// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
// 获取连接
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(conn != null){
//提交事务
conn.commit();
//释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose(Connection conn) {
try {
if(conn != null){
//回滚事务
conn.rollback();
//释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
(5) dao层代码 : AccountDao
package com.itheima.transfer.dao;
import com.itheima.transfer.utils.JdbcUtils;
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 account set money = money - ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
JdbcUtils.release(pstm, conn);
}
public void in(String inUser, int money) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
JdbcUtils.release(pstm,conn);
}
}
(6) service层代码 : AccountService
package com.itheima.transfer.service;
import com.itheima.transfer.dao.AccountDao;
import java.sql.SQLException;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
// 转出
ad.out(outUser, money);
// 转入
ad.in(inUser, money);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
(7) web层代码 : AccountWeb
package com.itheima.transfer.web;
import com.itheima.transfer.service.AccountService;
public class AccountWeb {
public static void main(String[] args) {
// 模拟数据 : Jack 给 Rose 转账 100
String outUser = "Jack";
String inUser = "Rose";
int money = 100;
AccountService as = new AccountService();
boolean result = as.transfer(outUser, inUser, money);
if (result == false) {
System.out.println("转账失败!");
} else {
System.out.println("转账成功!");
}
}
}
引入事务:
案例中的转账涉及两个DML操作: 一个转出,一个转入。
这些操作是需要具备原子性的,不可分割
不然就有可能出现数据修改异常情况。
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
// 转出
ad.out(outUser, money);
// 模拟转账过程中的异常
int i = 1/0;
// 转入
ad.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
常规解决方案
常规方案的实现
基于上面给出的前提, 大家通常想到的解决方案是 :
- 从service层将connection对象向dao层传递
- 加锁
以下是代码实现修改的部分:
(1 ) AccountService 类
package com.itheima.transfer.service;
import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
//线程并发情况下,为了保证每个线程使用各自的connection,故加锁
synchronized (AccountService.class) {
Connection conn = null;
try {
conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
// 转出
ad.out(conn, outUser, money);
// 模拟转账过程中的异常
// int i = 1/0;
// 转入
ad.in(conn, inUser, money);
//事务提交
JdbcUtils.commitAndClose(conn);
} catch (Exception e) {
e.printStackTrace();
//事务回滚
JdbcUtils.rollbackAndClose(conn);
return false;
}
return true;
}
}
}
(2) AccountDao 类 (这里需要注意的是: connection不能在dao层释放,要在service层,不然在dao层释放,service层就无法使用了)
package com.itheima.transfer.dao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AccountDao {
public void out(Connection conn, String outUser, int money) throws SQLException{
String sql = "update account set money = money - ? where name = ?";
//注释从连接池获取连接的代码,使用从service中传递过来的connection
// Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
//连接不能在这里释放,service层中还需要使用
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
public void in(Connection conn, String inUser, int money) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
// Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
}
常规方案的弊端
上述方式我们看到的确按要求解决了问题,但是仔细观察,会发现这样实现的弊端:
-
直接从service层传递connection到dao层, 造成代码耦合度提高
-
加锁会造成线程失去并发性,程序性能降低
ThreadLocal解决方案:
ThreadLocal方案的实现
像这种需要在项目中进行数据传递和线程隔离的场景,我们不妨用ThreadLocal来解决:
(1) 工具类的修改: 加入ThreadLocal
package com.itheima.transfer.utils;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class JdbcUtils {
//ThreadLocal对象 : 将connection绑定在当前线程中
private static final ThreadLocal<Connection> tl = new ThreadLocal();
// c3p0 数据库连接池对象属性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
// 获取连接
public static Connection getConnection() throws SQLException {
//取出当前线程绑定的connection对象
Connection conn = tl.get();
if (conn == null) {
//如果没有,则从连接池中取出
conn = ds.getConnection();
//再将connection对象绑定到当前线程中
tl.set(conn);
}
return conn;
}
//释放资源
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() {
try {
Connection conn = getConnection();
//提交事务
conn.commit();
//解除绑定
tl.remove();
//释放连接
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose() {
try {
Connection conn = getConnection();
//回滚事务
conn.rollback();
//解除绑定
tl.remove();
//释放连接
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
(2) AccountService类的修改:不需要传递connection对象
package com.itheima.transfer.service;
import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
Connection conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
// 转出 : 这里不需要传参了 !
ad.out(outUser, money);
// 模拟转账过程中的异常
// int i = 1 / 0;
// 转入
ad.in(inUser, money);
//事务提交
JdbcUtils.commitAndClose();
} catch (Exception e) {
e.printStackTrace();
//事务回滚
JdbcUtils.rollbackAndClose();
return false;
}
return true;
}
}
(3) AccountDao类的修改:照常使用
package com.itheima.transfer.dao;
import com.itheima.transfer.utils.JdbcUtils;
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 account set money = money - ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,outUser);
pstm.executeUpdate();
//照常使用
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
public void in(String inUser, int money) throws SQLException {
String sql = "update account set money = money + ? where name = ?";
Connection conn = JdbcUtils.getConnection();
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,money);
pstm.setString(2,inUser);
pstm.executeUpdate();
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
}
ThreadLocal方案的好处
从上述的案例中我们可以看到, 在一些特定场景下,ThreadLocal方案有两个突出的优势:
-
传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题
-
线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
通过以上的学习,相信同学们对ThreadLocal的作用有了一定的认识
那么,现在我们来看一下ThreadLocal的内部结构,
以便后文 探究 它能够实现 线程数据隔离 的原理:
内部结构:
对于 ThreadLocal类,在JDK发展改进的过程中,出现过 两种设计方式:
JDK1.8 前:
每个
ThreadLocal
类 都创建一个Map
,
然后用 线程的IDthreadID
作为Map
的key
,要存储的局部变量 作为Map
的value
,
这样就能达到 各个线程 的 局部变量隔离 的效果
JDK1.8 后:
每个
Thread
维护一个ThreadLocalMap
哈希表,
这个哈希表的key
是ThreadLocal
实例本身,value
才是 真正要存储的值
图示:
那么,本人对于上图做些说明:
说明:
- 每个 Thread线程 内部都有一个Map
(准确来说是两个ThreadLocalMap,一个是本类,一个是用于继承子类)
- Map里面存储 ThreadLocal对象(key) 和 属于该线程的目标变量的副本(value)
- Thread内部的Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置 线程的变量值
- 对于 不同的线程,
每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰
或许有同学这时候就有疑问了:
jdk1.8之前的设计看起来蛮好的,而且由线程自己来维护本地变量副本,也比较方便些
那jdk1.8之后版本的设计 的 好处 是什么?
那么,本人现在来讲解下这样改进的好处:
好处:
- 因为之前的 存储数量 由
Thread
的数量 决定,现在是由ThreadLocal
的数量 决定
这样设计之后 每个Map
存储的Entry
数量 就会 变少- 当
Thread
销毁 之后,对应的ThreadLocalMap
也会随之 被回收,能 减少内存的使用
那么,知道了ThreadLocal的 概念用法 和 适用场景,本人现在来带同学们解析下 ThreadLocal类 的 核心源码:
核心源码 解析:
除了 构造方法 之外, ThreadLocal对外暴露的方法有以下4个:
方法声明 | 描述 |
---|---|
public void set( T value) | 设置 当前线程绑定的局部变量 |
public T get() | 获取 当前线程绑定的局部变量 |
public void remove() | 移除 当前线程绑定的局部变量 |
那么,本人先来展示下 构造方法 的 源码:
构造方法:
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
在讲解其它方法前,本人要先来讲解一个protected方法 —— 方法:
initialValue() 方法
源码:
protected T initialValue() {
return null;
}
作用:
此方法的作用是:
返回 该线程 局部变量的 初始值
说明:
- 这个方法是一个
延迟调用
方法,
从之后的代码我们能看到在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
- 这个方法缺省实现,直接返回一个
null
。- 如果想要一个 除null之外 的 初始值,可以重写此方法
(备注: 该方法是一个protected
的方法,显然是为了 让子类覆盖 而设计的)
get() 方法:
源码:
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用 initialValue()方法 进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
Thread t = Thread.currentThread();
/*
获取此线程中 维护的ThreadLocalMap对象
*/
ThreadLocalMap map = getMap(t);
/*
若该map存在:
以当前的ThreadLocal对象为 key,调用getEntry获取对应的 存储实体e
若该对象存在:
则转换类型后 返回该对象
若该map不存在:
则说明 目标线程 没有 维护的ThreadLocalMap对象,调用setInitialValue进行初始化
*/
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
return setInitialValue();
}
那么,现在本人来总结下 get()方法 的 执行流程:
执行流程:
- 获取 当前线程对象
- 根据 当前线程,获取 一个Map
- 如果 获取的Map不为空,则在Map中以ThreadLocal的引用 作为
key
来在Map中获取对应的value e,否则转到 步骤5- 如果 e不为null,则 返回e.value,否则转到 步骤5
- Map为空 或者 e为空:
则通过 initialValue()方法 获取 初始值value,
然后用 ThreadLocal的引用和value 作为 firstKey和firstValue 创建一个 新的Map
总结:
先获取 当前线程 的 ThreadLocalMap 变量,
如果 存在 则返回 恰当类型的值,不存在 则创建并返回 初始值
那么,本人来展示并讲解下 get中所调用的 两个方法:
getMap 方法:
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
setInitialValue 方法:
/**
* set的变样实现,用于初始化值initialValue,
* 用于代替防止用户重写set()方法
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
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)并将此实体entry作为第一个值存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}
/**
* 创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set() 方法:
源码:
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// map存在,则调用map.set设置此实体entry
map.set(this, value);
else
/*
1)当前线程Thread 不存在ThreadLocalMap对象
2)则调用createMap进行ThreadLocalMap对象的初始化
3)并将此实体entry作为第一个值存放至ThreadLocalMap中
*/
createMap(t, value);
}
相信看了上述源码的同学,已经对执行流程了然于心了
为了排版整齐,本人还是来总结下这个方法的执行流程吧:
执行流程:
- 获取当前线程,并根据 当前线程 获取 对应的Map
- 如果获取的 Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
- 如果 Map为空,则给该线程 创建 Map,并 设置初始值
remove() 方法
源码:
/**
* 删除 当前线程所拥有的ThreadLocal副本键值对
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
执行流程:
- 获取当前线程,并根据 当前线程 获取 一个Map
- 如果获取的 Map不为空,则移除 当前ThreadLocal对象 对应的 entry
那么,相信很多同学对于 ThreadLocal类 的理解已经很透彻了
但是,这还是远远不够的!
在上文中,我们都能看得出:
对于 多线程 下的 ThreadLocal类的对象,会根据线程的创建而构造一个Map
而我们也知道,若是想要 提升性能,就得从 底层的数据结构 入手
那么,这个“Map”的学习,就变得不得不了解了
那么,为了想要真正搞懂ThreadLocal,就必须了解 这个“Map” —— ThreadLocalMap:
ThreadLocalMap类:
首先,本人来展示下 ThreadLocalMap类 的 继承体系:
继承体系:
从上图中,我们可以看出:
ThreadLocalMap类 中的键值对,是 Entry类,
但是,这个Entry,并不是HashMap中的内部类Entry,而是 ThreadLocal类的内部类ThreadLocalMap类的内部类
而这个Entry类 继承自 WeakReference类,便于垃圾回收
接下来,本人来带同学们解读下 源码:
核心源码 解读:
首先,本人来展示下 ThreadLocal类 的成员变量:
成员变量:
/**
* 初始容量 —— 必须是2的整次幂
* 默认为16
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table,Entry类的定义
* 数组长度必须是2的幂
*/
private Entry[] table;
/**
* 数组里面entrys的个数,
* 可以用于判断table当前使用量是否超过负因子。
*/
private int size = 0;
/**
* 扩容阈值,
* 表使用量大于它的时候进行扩容
*/
private int threshold; // Default to 0
/**
* 阈值设置为长度的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
从上文中,我们能够看出:
初始默认容量
为 16- ThreadLocalMap 的
扩容阈值
是 容量的2/3
接下来,本人来展示下 ThreadLocalMap类 的 构造方法:
构造方法:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
/*
计算所传firstKey所在table中的下标
并将 firstValue 作为值,存储并作为 table当前单元的首元素
*/
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY); // 计算阈值(初始容量的2/3)
}
/**
* Construct a new map including all Inheritable ThreadLocals
* from given parent map. Called only by createInheritedMap.
*
* @param parentMap the map associated with parent thread.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
/*
将 parentMap中的键值对,转存到 新的table中
*/
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) {
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++;
}
}
}
}
这时候可能就有同学联想到本人之前博文中所讲解的HasdMap了,于是提出如下问题:
这个会发生安全问题吧?
没有加锁,无论是 拉链 还是 红黑树 的转存,都是有线程安全的!
不不不,同学,ThreadLocal是 线程隔离
的,是 不会存在线程安全问题 的!
那么,本人还是从 增删查
角度 来分析源码:
首先是代表 增 的 set方法
set 方法:
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
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;
// 根据 table长度 和 所传key的哈希值,
// 计算在当前table中的下标
int i = key.threadLocalHashCode & (len-1);
/*
提供 “线性探测法”,查找当前key是否存在map中:
1、若 存在,则 将原值覆盖
2、若 不存在,则
*/
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(); // 将原长度 扩容为 2倍
}
关于set方法,有一个非常重要的问题 —— hash冲突
由于这个问题十分重要,在面试中常常作为面试常考点出现
那么,本人就在下文中的 总结区 进行详细的讲解!
接下来,是代表 “删” 的 remove方法:
remove 方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
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); // 删除过期的entry
return;
}
}
}
从源码中,我们可以看得出:
ThreadLocalMap 中的
remove方法
的 实现逻辑 很简单:
找到 目标ThreadLocal 的 位置,并进行 清理
那么,键值对 在 ThreadLocalMap类 中,是如何存储的呢?
答曰:
Entry类
键值对 存储结构 —— Entry类:
源码:
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,使用弱引用,
可以将ThreadLocal对象 的 生命周期 和 线程生命周期 解绑,
持有对 ThreadLocal 的 弱引用,可以使得ThreadLocal在 没有其他强引用 的时候 被回收掉,
这样可以避免因为 线程得不到销毁,导致 ThreadLocal对象无法被回收
接下来,本人来讲解下代表 查 的 getEntry 方法:
getEntry 方法:
private final int threadLocalHashCode = nextHashCode();
/**
* 从0开始,获取下一个要给出的哈希码。
* 自动更新。
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 获取 与键关联 的 Entry。
* 此方法本身仅处理快速路径:
* 直接命中现有密钥。
* 否则,它将中继到getEntryAfterMiss。
* 旨在通过部分使此方法易于操作来最大程度地提高直接打击的性能。
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1); // 计算当前key应在下标
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 若是没找到,则 继续查找
return getEntryAfterMiss(key, i, e);
}
关于getEntry方法,没有什么需要细讲之处
本人来讲解下其中 常量HASH_INCREMENT
的 取值原因:
HASH_INCREMENT 小故事:
说法一:
与斐波那契散列有关,
0x61c88647对应的十进制为1640531527。
斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,
如果把这个值给转为带符号的int,则会得到-1640531527。
还有一种说法 是:
0x61c88647如果单位是秒的话,那么化成年约为51年,
如果按照第一台计算机诞生1946年算,这是数字恰好是ThreadLocal上写的年份 —— 1997年
(此处故事摘自《jdk源码之ThreadLocal》)
我们都知道:
作为存储 K-V键值对 的数据结构,
最重要得到一点就是hash冲突
那么,本人现在来讲解下 在 ThreadLocalMap类 中,hash冲突 是如何解决的:
hash冲突 解决:
说到hash冲突,根据本人之前博文中所讲解的HashMap来看,一般只会出现在 添加新元素
的方法中
那么,我们再来回顾下 ThreadLocal类 的 set方法 的 源码:
set方法 源码再现:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else // 懒加载模式
createMap(t, value);
}
那么,我们再来看看其中的 createMap方法:
createMap 方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
继续跟踪,接下来是 ThreadLocalMap类 的 构造方法:
ThreadLocalMap类 构造方法:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
// 计算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 设置 值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
// 设置 阈值
setThreshold(INITIAL_CAPACITY);
}
对于上述代码,本人来讲解下 计算索引 的 原理:
上文方法中的 计算索引 的那行代码
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
而上行代码中的
& (INITIAL_CAPACITY - 1)
,
不用本人多说,就是 约束 计算出的索引的范围
那么,本人就针对 firstKey.threadLocalHashCode
来讲解下 原理:
threadLocalHashCode 属性:
首先,来看一下 相关源码:
private static final int HASH_INCREMENT = 0x61c88647;
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
关于这里的代码,在上文中本人已经讲解地很详细了
而这里的 HASH_INCREMENT 保证了
哈希码 能 均匀分布 在 2的n次方 的数组里
也就是Entry[] table 中,这样做可以尽量 避免hash冲突
然后就是 ThreadLocalMap类 的 set方法 中的下方 代码块:
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;
}
}
如上的循环中,使用了 线性探测法
来解决哈希冲突
线性探测法·具体流程:
线性探测法 的地址增量di = 1, 2, ... 其中,i 为 探测次数。
该方法一次探测下一个地址,直到有空的地址后插入,
若整个空间都找不到空余的地址,则产生 溢出。
假设当前table长度为16,也就是说如果计算出来key的hash值为14,
如果table[14]上已经有值,并且其key与当前key不一致,
那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,
这个时候如果还是冲突会回到0,取table[0]
以此类推,直到可以插入
... ...
最后,本人稍作总结:
总结:
ThreadLocal 通过 如下方式处理 哈希冲突
- 元素的插入是根据
线性探测法
计算 数组下标 的- 扩容后的
容量
和阈值
是 扩容前的 2倍
(相关源码未展示,有兴趣的同学请自行解析)
那么,至此,ThreadLocal类 讲解完毕!