• 深入理解ThreadLocal


    1、为什么想要了解ThreadLocal

    1. @Component
    2. public class LoginUserInterceptor implements HandlerInterceptor {
    3. public static ThreadLocal loginUser = new ThreadLocal<>();
    4. @Override
    5. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    6. MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthConstant.LOGIN_USER);
    7. if(attribute != null){
    8. loginUser.set(attribute);
    9. return true;
    10. }else{
    11. //没有登录就去登录
    12. request.getSession().setAttribute("msg","请先进行登录");
    13. response.sendRedirect("http://auth.gulimall.com:88/login.html");
    14. }
    15. return false;
    16. }
    17. }

    拦截器中,使用ThreadLocal保存了session中的用户信息,因为ThreadLocal是线程隔离的,整个ThreadLocal链的都可以共享这里边set的数据。

    应用如下:

    在某个接口下的所有实现中,可以直接获取当前登录的用户信息。

    MemberResponseVO loginUser = LoginUserInterceptor.loginUser.get();

    以往,遇到此场景,用户登录时,会将用户信息保存到redis中,取值的话,直接从redis中获取。所以,想了解下,为什么使用ThreadLocal,以及其特点和用途。

    2、带着面试题去理解ThreadLocal

    著作权归https://pdai.tech所有。 链接:Java 并发 - ThreadLocal详解 | Java 全栈知识体系

    • 什么是ThreadLocal? 用来解决什么问题的?
    • 说说你对ThreadLocal的理解
    • ThreadLocal是如何实现线程隔离的?
    • 为什么ThreadLocal会造成内存泄露? 如何解决
    • 还有哪些使用ThreadLocal的应用场景?

    3、ThreadLocal简介

    该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

     总结:ThreadLocal是一个将在多线程中为每一个线程创建的变量副本的类;当使用ThreadLocal来维护变量时,ThreadLocal会为每一个线程创建单独的变量副本,避免因多线程操作共享变量而导致的数据不一致的情况。

    数据库connection案例

    提到ThreadLocal应用最多的就是session管理和数据库链接管理,这里以数据访问为例帮助更好理解ThreadLocal;

    • 如下数据库管理类在单线程使用是没有任何问题
      1. class ConnectionManager {
      2. private static Connection connect = null;
      3. public static Connection openConnection() {
      4. if (connect == null) {
      5. connect = DriverManager.getConnection();
      6. }
      7. return connect;
      8. }
      9. public static void closeConnection() {
      10. if (connect != null)
      11. connect.close();
      12. }
      13. }

      如上:在多线程的环境下会出现问题。

    1、这里边的两个方法都没有同步,很可能在openConnection的方法中多次创建connect

    2、由于connect是共享变量,那么必然在调用connect的地方需要使用同步来保证线程安全,很可能一个线程在使用connect进行数据库操作,另外一个线程调用closeConnection关闭连接。

    解决

    为了解决上述线程安全问题:

    1、互斥同步。【这段代码的两个方法进行同步处理,并且调用connection的地方需要进行同步处理,比如Synchronized或者是ReentrantLock互斥锁

    2、考虑下是否必须将connect遍历进行共享?

    事实上,是不需要的。假如每个线程都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改,即修改后的代码是这样的。

    即 将Connection对象改为非静态的。

    1. class ConnectionManager {
    2. private Connection connect = null;
    3. public Connection openConnection() {
    4. if (connect == null) {
    5. connect = DriverManager.getConnection();
    6. }
    7. return connect;
    8. }
    9. public void closeConnection() {
    10. if (connect != null)
    11. connect.close();
    12. }
    13. }
    14. class Dao {
    15. public void insert() {
    16. ConnectionManager connectionManager = new ConnectionManager();
    17. Connection connection = connectionManager.openConnection();
    18. // 使用connection进行操作
    19. connectionManager.closeConnection();
    20. }
    21. }

    改成非静态,这样处理确实没有任何问题,由于每次都是在方法内部创建的connection,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的问题。

    由于在方法中需要频繁开启关闭数据库连接,这样不仅仅严重影响程序程序的执行效率,还可能导致服务器压力增大。

    那么什么时候会存在线程安全问题呢?

    (1)多线程并发条件

    (2)有共享的数据

    (3)多线程操作共享的数据

    满足以上三个条件,就会存在线程安全问题。

    解决:

    (1)多线程排队执行,就是所谓的同步执行。

            1、同步代码块

    1. /**
    2. 这样就会使小红进来取钱,小明只能在外面看着
    3. 把出现线程安全问题的核心代码给上锁。
    4. */
    5. synchronized (this) {
    6. //1、判断账户是否够钱
    7. if (this.money >= money) {
    8. //2、取钱
    9. System.out.println(name + "来取钱成功,吐出:" + money);
    10. //3、更新余额
    11. this.money -= money;
    12. System.out.println(name + "取钱后剩余:" + this.money);
    13. }else {
    14. System.out.println(name+"来取钱,余额不足!");
    15. }

    作用:把出现线程安全问题的核心代码给上锁。

    原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

            2、同步方法

    1. //把出现线程安全问题的核心方法给上锁。
    2. public synchronized void drawMoney(double money) {
    3. //1、先判断是谁来取钱,线程的名字就是人名
    4. String name = Thread.currentThread().getName();
    5. //2、判断账户是否够钱
    6. if (this.money >= money) {
    7. //3、取钱
    8. System.out.println(name + "来取钱成功,吐出:" + money);
    9. //4、更新余额
    10. this.money -= money;
    11. System.out.println(name + "取钱后剩余:" + this.money);
    12. }else {
    13. System.out.println(name+"来取钱,余额不足!");
    14. }

     原理:

    同步方法其实也是有隐式的锁对象的,锁的作用范围是整个方法。

    如果方法是实例方法:同步方法默认使用this作为锁的对象。但是代码要高度面向对象。

    如果方法是静态方法:同步方法默认用类名.class作为锁的对象。

            3、Lock锁

    为了更加清晰表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活方便。

    Lock实现提供比synchronized方法和语句更广泛的锁定操作。

    方法名称

    说明

    public ReentrantLock​()

    获得Lock锁的实现类对象

     Lock的API

    方法名称

    说明

    void lock()

    获得锁

    void unlock()

    释放锁

    1. //final修饰后:锁对象是唯一和不可替换的
    2. private final Lock lock = new ReentrantLock();
    3. public void drawMoney(double money) {
    4. //1、先判断是谁来取钱,线程的名字就是人名
    5. String name = Thread.currentThread().getName();
    6. lock.lock();
    7. try {
    8. //2、判断账户是否够钱
    9. if (this.money >= money) {
    10. //3、取钱
    11. System.out.println(name + "来取钱成功,吐出:" + money);
    12. //4、更新余额
    13. this.money -= money;
    14. System.out.println(name + "取钱后剩余:" + this.money);
    15. }else {
    16. System.out.println(name+"来取钱,余额不足!");
    17. }
    18. } finally {
    19. lock.unlock();
    20. }
    21. }

    ThreadLocal登场

    那么在同步互斥下使用ThreadLocal最合适了。以为threadlocal在每个线程中对该变量会创建出一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互相不影响,这样一来就不存在线程安全问题,也不会频繁开启和关闭连接,进而也不会影响程序的执行性能。

    代码如下所示:

    1. import java.sql.Connection;
    2. import java.sql.DriverManager;
    3. import java.sql.SQLException;
    4. public class ConnectionManager {
    5. private static final ThreadLocal dbConnectionLocal = new ThreadLocal() {
    6. @Override
    7. protected Connection initialValue() {
    8. try {
    9. return DriverManager.getConnection("", "", "");
    10. } catch (SQLException e) {
    11. e.printStackTrace();
    12. }
    13. return null;
    14. }
    15. };
    16. public Connection getConnection() {
    17. return dbConnectionLocal.get();
    18. }
    19. }

    ThreaLocal的JDK文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread。

    如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。

    但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

    ThreadLocal原理

    如何实现线程隔离

    Thread的get()主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals, 负责存储当前线程的关于Connection的对象,并且返回存储的泛型类型Value。

    上边的案例:dbConnectionLocal(以上述例子中为例) 这个变量为Key, 以新建的Connection对象为Value; 这样的话, 线程第一次读取的时候如果不存在就会调用ThreadLocal的initialValue方法创建一个Connection对象并且返回;

    具体关于为线程分配变量副本的代码如下:

    关于get()源码:

    1. /**
    2. * Returns the value in the current thread's copy of this
    3. * thread-local variable. If the variable has no value for the
    4. * current thread, it is first initialized to the value returned
    5. * by an invocation of the {@link #initialValue} method.
    6. *
    7. * @return the current thread's value of this thread-local
    8. */
    9. public T get() {
    10. Thread t = Thread.currentThread();
    11. ThreadLocalMap map = getMap(t);
    12. if (map != null) {
    13. ThreadLocalMap.Entry e = map.getEntry(this);
    14. if (e != null) {
    15. @SuppressWarnings("unchecked")
    16. T result = (T)e.value;
    17. return result;
    18. }
    19. }
    20. return setInitialValue();
    21. }
    22. ThreadLocalMap getMap(Thread t) {
    23. return t.threadLocals;
    24. }

    如果没有泛型类型,则setInitialValue();

    源码:

    1. private T setInitialValue() {
    2. T value = initialValue();
    3. Thread t = Thread.currentThread();
    4. ThreadLocalMap map = getMap(t);
    5. if (map != null)
    6. map.set(this, value);
    7. else
    8. createMap(t, value);
    9. return value;
    10. }
    11. void createMap(Thread t, T firstValue) {
    12. t.threadLocals = new ThreadLocalMap(this, firstValue);
    13. }

    这样我们也可以不实现initialValue, 将初始化工作放到DBConnectionFactory的getConnection方法中:

    1. public Connection getConnection() {
    2. Connection connection = dbConnectionLocal.get();
    3. if (connection == null) {
    4. try {
    5. connection = DriverManager.getConnection("", "", "");
    6. dbConnectionLocal.set(connection);
    7. } catch (SQLException e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. return connection;
    12. }

    那么我们看过代码之后,就很清晰知道了为什么ThreadLocal能实现变量的多线程隔离了;其实就是利用了Map的数据结构给当前线程缓存了,要使用的时候,就从本线程的ThreadLocals对象中获取就可以了,避免了频繁的创建和销毁。key就是当前的线程。

    当然了,在当前线程下,获取当前线程里边的Map对象并操作,那么就肯定没有线程的并发问题了,当然能做到变量的线程隔离了;

    TheadLocalMap

    什么是ThreadLocalMap,为什么要用这个对象呢?

    本质上来讲,它就是一个Map,但是这个ThreadLocalMap与我们平时见到的Map有点不一样。

    它没有实现Map接口;

    它没有实现public方法,最多有一个default的构造方法,因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用,属于静态内部类。

    ThreadLocalMap的Entry实现继承了WeakReference>

    该方法仅仅用了一个Entry数组来存储Key,Value;Entry并不是链式形式,而是每个bucker里面仅仅放一个Entry;

    要了解ThreadLocalMap的实现, 我们先从入口开始, 就是往该Map中添加一个值:

    1. private void set(ThreadLocal key, Object value) {
    2. // We don't use a fast path as with get() because it is at
    3. // least as common to use set() to create new entries as
    4. // it is to replace existing ones, in which case, a fast
    5. // path would fail more often than not.
    6. Entry[] tab = table;
    7. int len = tab.length;
    8. int i = key.threadLocalHashCode & (len-1);
    9. for (Entry e = tab[i];
    10. e != null;
    11. e = tab[i = nextIndex(i, len)]) {
    12. ThreadLocal k = e.get();
    13. if (k == key) {
    14. e.value = value;
    15. return;
    16. }
    17. if (k == null) {
    18. replaceStaleEntry(key, value, i);
    19. return;
    20. }
    21. }
    22. tab[i] = new Entry(key, value);
    23. int sz = ++size;
    24. if (!cleanSomeSlots(i, sz) && sz >= threshold)
    25. rehash();
    26. }

    先进行简单的分析, 对该代码表层意思进行解读:

    • 看下当前threadLocal的在数组中的索引位置 比如: i = 2, 看 i = 2 位置上面的元素(Entry)的Key是否等于threadLocal 这个 Key, 如果等于就很好说了, 直接将该位置上面的Entry的Value替换成最新的就可以了;
    • 如果当前位置上面的 Entry 的 Key为空, 说明ThreadLocal对象已经被回收了, 那么就调用replaceStaleEntry
    • 如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 > 阈值的时候对当前的Table进行重新哈希 所以, 该HashMap是处理冲突检测的机制是向后移位, 清除过期条目 最终找到合适的位置;

    了解完Set方法, 后面就是Get方法了:

    1. private Entry getEntry(ThreadLocal key) {
    2. int i = key.threadLocalHashCode & (table.length - 1);
    3. Entry e = table[i];
    4. if (e != null && e.get() == key)
    5. return e;
    6. else
    7. return getEntryAfterMiss(key, i, e);
    8. }

    先找到ThreadLocal的索引位置, 如果索引位置处的entry不为空并且键与threadLocal是同一个对象, 则直接返回; 否则去后面的索引位置继续查找。

    ThreadLocal造成的内存泄漏问题

    1. package com.atguigu.gulimall.product;
    2. import java.util.concurrent.LinkedBlockingQueue;
    3. import java.util.concurrent.ThreadPoolExecutor;
    4. import java.util.concurrent.TimeUnit;
    5. /**
    6. * @author pshdhx
    7. * @date 2022-09-06 15:39
    8. * @Des
    9. * @Method
    10. * @Summary
    11. */
    12. public class TestThreadLocal {
    13. static class LocalVariable {
    14. private Long[] a = new Long[1024 * 1024];
    15. }
    16. // (1)
    17. final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
    18. new LinkedBlockingQueue<>());
    19. // (2)
    20. final static ThreadLocal localVariable = new ThreadLocal();
    21. public static void main(String[] args) throws InterruptedException {
    22. // (3)
    23. Thread.sleep(50 * 4);
    24. for (int i = 0; i < 5000; ++i) {
    25. poolExecutor.execute(new Runnable() {
    26. public void run() {
    27. // (4)
    28. localVariable.set(new LocalVariable());
    29. // (5)
    30. System.out.println("use local varaible" + localVariable.get());
    31. localVariable.remove();
    32. }
    33. });
    34. }
    35. // (6)
    36. System.out.println("pool execute over");
    37. }
    38. }

    如果用线程池来操作ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的LocalVariable对象也不会释放, 就造成了内存泄露; 如果LocalVariable对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable对象的大小;

    所以, 为了避免出现内存泄露的情况, ThreadLocal提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 ThreadLocalMap 的remove方法:

    1. private void remove(ThreadLocal key) {
    2. Entry[] tab = table;
    3. int len = tab.length;
    4. int i = key.threadLocalHashCode & (len-1);
    5. for (Entry e = tab[i];
    6. e != null;
    7. e = tab[i = nextIndex(i, len)]) {
    8. if (e.get() == key) {
    9. e.clear();
    10. expungeStaleEntry(i);
    11. return;
    12. }
    13. }
    14. }

    找到Key对应的Entry, 并且清除Entry的Key(ThreadLocal)置空, 随后清除过期的Entry即可避免内存泄露。

    ThreadLocal应用场景

    除了上述的数据库管理类的例子,我们再看看其它一些应用:

    每个线程维护了一个“序列号

    再回想上文说的,如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。

    1. public class SerialNum {
    2. // The next serial number to be assigned
    3. private static int nextSerialNum = 0;
    4. private static ThreadLocal serialNum = new ThreadLocal() {
    5. protected synchronized Object initialValue() {
    6. return new Integer(nextSerialNum++);
    7. }
    8. };
    9. public static int get() {
    10. return ((Integer) (serialNum.get())).intValue();
    11. }
    12. }

    Session的管理

    经典的另外一个例子:

    1. private static final ThreadLocal threadSession = new ThreadLocal();
    2. public static Session getSession() throws InfrastructureException {
    3. Session s = (Session) threadSession.get();
    4. try {
    5. if (s == null) {
    6. s = getSessionFactory().openSession();
    7. threadSession.set(s);
    8. }
    9. } catch (HibernateException ex) {
    10. throw new InfrastructureException(ex);
    11. }
    12. return s;
    13. }

    java 开发手册中推荐的 ThreadLocal

    看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:

    1. import java.text.DateFormat;
    2. import java.text.SimpleDateFormat;
    3. public class DateUtils {
    4. public static final ThreadLocal df = new ThreadLocal(){
    5. @Override
    6. protected DateFormat initialValue() {
    7. return new SimpleDateFormat("yyyy-MM-dd");
    8. }
    9. };
    10. }
    11. DateUtils.df.get().format(new Date());

    参考文献

    Java 并发 - ThreadLocal详解 | Java 全栈知识体系

  • 相关阅读:
    1620、网络信号最好的坐标
    Alibaba针对“金九银十”推出的《Java岗位面试清单》,全是考点
    Elasticsearch索引中数据的增删改查与并发控制
    Datafaker生成模拟数据
    JMeter的使用
    MySQL XA事务文档翻译
    学习python第6天
    【博客718】时序数据库基石:LSM Tree(log-structured merge-tree)
    ArcGIS按点提取栅格
    Ubuntu 17.10的超震撼声音权限
  • 原文地址:https://blog.csdn.net/pshdhx/article/details/126720309