博客文章地址:ThreadLocal------原理分析及应用场景
个人博客主页:www.samsa-blog.top 欢迎各位掘友交流
之前自己搭建的博客,写博客都是自己记录学习知识体系的过程;现迁移到掘进,想扩充一下交际圈子,和各位掘友擦出思想的火花。
ThreadLocal 线程本地变量,创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面变量,从而避免了线程安全题。
一、实例
threadOne在set变量副本之后,get查看变量;
threadTwo在set变量副本之后,get查看变量,然后remove删除变量,再查看变量副本,已经删除。
threadOne会再等待3s查看自己的变量副本,发现没有删除,证明每一个线程对变量有一份独立的副本。
java
public class ThreadLocalTest {
// (1)创建ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<> () ;
public static void main(String[] args) {
// (2)创建线程one
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("thread one local variable");
System.out.println("thread one" + ":" + localVariable.get());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// (5)这里等待3s,目的是为了看threadTwo删除自己的变量之后,查看threadOne的本地副本是否被删除
System.out.println("\nwaiting 3s thread one" + ":" + localVariable.get());
}
});
// (3)创建线程two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("thread two local variable");
System.out.println("thread one" + ":" + localVariable.get());
localVariable.remove() ;
System.out.println("thread two remove after" + ":" + localVariable.get());
}
});
// (4) 启动两个线程
threadOne.start();
threadTwo.start();
}
}
// 执行结果:
thread one:thread one local variable
thread one:thread two local variable
thread two remove after:null
waiting 3s thread one:thread one local variable
二、ThreadLocal源码分析
- Thread 类中有两个变量:
threadLocals
和inheritableThreadLocals
,它们都是ThreadLocalMap类型的,而ThreadLocalMap是一个定制化的Hashmap; - 在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set方法或者get方法时才会创建;
- 其实每个线程的本地变量不是存放在ThreadLocal里面,而是存放在调用线程的threadLocals变量里,就是说ThreadLocal类型的本地变量存放在具体的线程内存空间中;
- ThreadLocal就是个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放,当线程调用它的get方法时,再从线程的threadLocals里面将其拿出来使用;
- 如果调用线程一直不终止, 那么这个本地变量会一直存放在调用线程的threadLocals里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的 remove方法 ,从当前线程threadLocals里面删除该本地变量。
- Thread 里面 threadLocals 设计为 map ,是因为每个线程可以关联多个 ThreadLocal 变量。
2.1 set方法
java
public void set(T value) {
// (1)获取当前线程
Thread t = Thread.currentThread();
// (2)将当前线程作为key,去查找对应的线程变量
ThreadLocalMap map = getMap(t);
if (map != null) // (3)如果不为null, 则覆盖
map.set(this, value);
else // (4)如果不存在,说明是第一次调用set方法,则创建当前线程的 threadLocals
createMap(t, value);
}
// java.lang.ThreadLocal#getMap (2)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 返回当前线程的 threadLocals
}
// java.lang.ThreadLocal#createMap (4)
void createMap(Thread t, T firstValue) {
// 构建 ThreadLocalMap,赋值给当前线程的threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
2.2 get方法
java
public T get() {
// (5)获取当前线程
Thread t = Thread.currentThread();
// (6)将当前线程作为key,去查找对应的线程变量
ThreadLocalMap map = getMap(t);
// (7)如果threadLocals 不为 null ,则返回对应的本地变量
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// (8)在threadLocals为 null的情况下,初始化为null,再返回
return setInitialValue();
}
// java.lang.ThreadLocal#setInitialValue
private T setInitialValue() {
T value = initialValue(); // (8) null 初始化为null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) // (9) 如果当前线程的 threadLocals 变量不为空
map.set(this, value);
else // (10)如果当前线程的 threadLocals 变量为空
createMap(t, value);
return value;
}
2.3 remove方法
java
// 比较简单,获取当前线程的 threadLocals,不为空就删除
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
三、lnheritableThreadLocal
该类极其简单,继承了ThreadLcoal,重写getMap方法返回当前线程的inheritableThreadLocals
变量,重写createMap方法构建ThreadLocalMap赋值给inheritableThreadLocals
,重写childValue方法赋值变量给子线程本地变量。
java
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
3.1 代码实例
利用InheritableThreadLocal
在 parent thread
线程中设置本地变量,然后再 parent thread
线程、child thread
线程分别获取本地变量,我们发现,child thread
线程没有设置本地变量也获取到了变量信息。
java
public class InheritableThreadLocalTest {
public static void main(String[] args) {
// (1)parentThread 父线程
Thread parentThread = new Thread(new Runnable() {
// (2)InheritableThreadLocal
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
@Override
public void run() {
// (3)父线程中设置线程变量
threadLocal.set("hello world");
// (4)childThread 子线程
Thread childThread = new Thread(new Runnable() {
@Override
public void run() {
// (5)child Thread子线程获取 本地变量
System.out.println(Thread.currentThread().getName()+ " : " +threadLocal.get());
}
},"child Thread");
childThread.start();
// (6)parent Thread 父线程获取 本地变量
System.out.println(Thread.currentThread().getName()+ " : " +threadLocal.get());
}
},"parent Thread");
parentThread.start();
}
}
// 运行结果:
parent Thread : hello world
child Thread : hello world
-
跟踪代码(3):查看到
parent Thread
线程set方法最终赋值给inheritableThreadLocals
变量。 -
跟踪代码(4):
进入创建线程的构造方法中;可以看到,
child Thread
子线程在构造的时候,就会判断父线程的inheritableThreadLocals
变量是否为空,如果不为空就会获取父线程的变量并赋值给自身的inheritableThreadLocals
变量。 所以这也是为什么利用InheritableThreadLocal
类,可以使子线程获取到父线程的本地变量。
四、ThreadLocal在Mybatis源码中应用
利用ThreadLocal将当前会话SqlSession保存在本地变量中,这样,只要是在当前线程中,就一直使用该SqlSession对数据库操作。
同时保证线程隔离,即:每个线程都有自己的SqlSession会话,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
这里我直接使用源码中的:SqlSessionManager来做实例。
java
// 源码位置 : org.apache.ibatis.session.SqlSessionManager
/**
* SqlSessionFactory是工厂,SqlSession的工厂的产品
* SqlSessionManager 这种既实现工厂接口又实现工厂产品接口的类是很少见的。
*
* 这样SqlSessionManager将工厂和产品整合到一起后,提供了下面两点功能:
* 1、SqlSessionManager总能给出一个产品(从线程 ThreadLocal取出或者新建)并使用该产品完成相关的操作,
* 外部使用者不需要了解细节,因此省略了调用工厂生产产品的过程。
* 2、提供了产品复用的功能。工厂生产出的产品可以放入线程 ThreadLocal 保存(需要显式调用 startManagedSession 方法),
* 从而实现产品的复用。这样既保证了线程安全又提升了效率。
*
* 很多场景下,用户使用的是工厂生产出来的产品,而不关心产品是即时生产的还是之前生产后缓存的。
* 在这种情况下,可以参考 SqlSessionManager的设计,来提供一种更为高效的给出产品的方式
*/
public class SqlSessionManager implements SqlSessionFactory, SqlSession {
// 构造方法中传入的SqlSessionFactory对象
private final SqlSessionFactory sqlSessionFactory;
// 在构造方法中创建的SqlSession代理对象
private final SqlSession sqlSessionProxy;
// 该变量用来存储被代理的SqlSession对象 : 这个也可以理解为一个ThreadLocal的一个应用场景!
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
/**
* 这里可以看到:该构造方法是私有的,外部需要通过 newInstance方法间接调用它
* @param sqlSessionFactory 工厂
*/
private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
// 注意这里是代理对象!!!
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class},
new SqlSessionInterceptor());
}
/**
* 传递SqlSession工厂给构造方法;没有将SqlSession保存到localSqlSession中;
* 需要主动调用startManagedSession方法将当前SqlSession保存到localSqlSession中
*/
public static SqlSessionManager newInstance(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionManager(sqlSessionFactory);
}
@Override
public Connection getConnection() {
// ★★★ 可以看到 getConnection() 是从 localSqlSession 中获取的
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot get connection. No managed session is started.");
}
return sqlSession.getConnection();
}
/**
* 将当前SqlSession保存到本地变量ThreadLocal中
*/
public void startManagedSession() {
this.localSqlSession.set(openSession());
}
@Override
public SqlSession openSession() {
return sqlSessionFactory.openSession();
}
}
测试实例:
java
public class MybatisUtil {
private static SqlSessionManager sqlSessionManager;
/**
* 加载配置文件
*/
static{
try{
// mybatis 配置文件
Reader reader = Resources.getResourceAsReader("MybatisConfig.xml");
// SqlSessionFactory 会话工厂
SqlSessionFactory SqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 构建 SqlSessionManager
sqlSessionManager = SqlSessionManager.newInstance(SqlSessionFactory);
}catch(IOException e){
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
// 1. 线程1
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
// 主动调方法保存 会话信息, 后面都是使用同一个会话 这里就会调用ThreadLocal的set方法
sqlSessionManager.startManagedSession();
// 利用SqlSessionManager中的 getMapper方法获取 接口的代理对象
UserMapper userMapper = sqlSessionManager.getMapper(UserMapper.class);
// 查询
User user1 = userMapper.selectByPrimaryKey(1);
System.out.println(user1);
// 这里调用 ThreadLocal的get方法;也就是在当前线程中,set会话之后,在任何位置都会获取同一个会话。
System.out.println(sqlSessionManager.getConnection());
}
},"threadOne");
// 2. 线程2
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
sqlSessionManager.startManagedSession();
UserMapper userMapper = sqlSessionManager.getMapper(UserMapper.class);
User user2 = userMapper.selectByPrimaryKey(2);
System.out.println(user2);
System.out.println(sqlSessionManager.getConnection());
}
},"threadTwo");
threadOne.start();
threadTwo.start();
System.out.println("test end...");
}
}
// 执行结果:
User(id=2, name=tom, email=tom@163.com, age=19, sex=0, schoolname=heheda)
User(id=1, name=yogurt, email=123@163.com, age=18, sex=1, schoolname=heheda)
com.mysql.cj.jdbc.ConnectionImpl@64c78082
com.mysql.cj.jdbc.ConnectionImpl@25d0ab4d
Debug 两个线程的 sqlSessionManager.getConnection()
代码,并分别进入两个线程的该方法:
- 在线程一中:
- 在线程二中:
我们可以看到直接利用localSqlSession.get()
方法获取的会话对象和利用当前线程获取当前线程变量找到的会话对象是同一个。