ThreadLocal——原理分析及应用场景

博客文章地址: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 类中有两个变量:threadLocalsinheritableThreadLocals,它们都是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 代码实例

​ 利用InheritableThreadLocalparent 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()方法获取的会话对象和利用当前线程获取当前线程变量找到的会话对象是同一个。

相关推荐
秦淮渔火22 分钟前
Java_集合_单列集合Collection
java·开发语言
Hello Dam24 分钟前
【RocketMQ】RocketMQ发送不同类型消息
java·rocketmq·java-rocketmq·springboot
橘子海全栈攻城狮30 分钟前
【源码+文档+调试讲解】学生选课系统Python
java·开发语言·数据库·python·小程序·sqlite
何政@34 分钟前
Web后端开发原理!!!什么是自动配置???什么是起动依赖???
java·spring boot·spring
霍金的微笑36 分钟前
MYSQL(学习笔记)
java·前端·数据库
拾光师36 分钟前
Spring Cloud全解析:服务调用之OpenFeign简介
java
CopyLower41 分钟前
什么东西可以当做GC Root,跨代引用如何处理?
java·jvm·算法
葡萄城技术团队42 分钟前
如何借助Java批量操作Excel文件?
java·python·excel
爬山算法1 小时前
Maven(1)什么是Maven?
java·maven
计算机学姐1 小时前
基于SpringBoot+Vue的旅游攻略平台管理系统
java·vue.js·spring boot·后端·intellij-idea·mybatis·旅游