深入理解ThreadLocal

1、为什么想要了解ThreadLocal

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

在拦截器中,使用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;

  • 如下数据库管理类在单线程使用是没有任何问题
  • class ConnectionManager {
        private static Connection connect = null;
    
        public static Connection openConnection() {
            if (connect == null) {
                connect = DriverManager.getConnection();
            }
            return connect;
        }
    
        public static void closeConnection() {
            if (connect != null)
                connect.close();
        }
    }

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

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

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

解决

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

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

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

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

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

class ConnectionManager {
    private Connection connect = null;

    public Connection openConnection() {
        if (connect == null) {
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public void closeConnection() {
        if (connect != null)
            connect.close();
    }
}

class Dao {
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();

        // 使用connection进行操作

        connectionManager.closeConnection();
    }
}

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

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

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

(1)多线程并发条件

(2)有共享的数据

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

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

解决:

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

        1、同步代码块

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

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

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

        2、同步方法

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

 原理:

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

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

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

        3、Lock锁

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

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

方法名称

说明

public ReentrantLock​()

获得Lock锁的实现类对象

 Lock的API

方法名称

说明

void lock()

获得锁

void unlock()

释放锁

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

ThreadLocal登场

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

代码如下所示:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {
    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection("", "", "");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public Connection getConnection() {
        return dbConnectionLocal.get();
    }
}

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()源码:

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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

源码:

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

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

public Connection getConnection() {
    Connection connection = dbConnectionLocal.get();
    if (connection == null) {
        try {
            connection = DriverManager.getConnection("", "", "");
            dbConnectionLocal.set(connection);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return connection;
}

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

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

TheadLocalMap

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

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

它没有实现Map接口;

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

ThreadLocalMap的Entry实现继承了WeakReference<ThreadLocal<?>>

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

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

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();
        }

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

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

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

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);
        }

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

ThreadLocal造成的内存泄漏问题

package com.atguigu.gulimall.product;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author pshdhx
 * @date 2022-09-06 15:39
 * @Des
 * @Method
 * @Summary
 */
public class TestThreadLocal {

    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024];
    }

    // (1)
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        // (3)
        Thread.sleep(50 * 4);
        for (int i = 0; i < 5000; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    // (4)
                    localVariable.set(new LocalVariable());
                    // (5)
                    System.out.println("use local varaible" + localVariable.get());
                    localVariable.remove();
                }
            });
        }
        // (6)
        System.out.println("pool execute over");
    }
}

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

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

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;
        }
    }
}

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

ThreadLocal应用场景

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

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

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

public class SerialNum {
    // The next serial number to be assigned
    private static int nextSerialNum = 0;

    private static ThreadLocal serialNum = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new Integer(nextSerialNum++);
        }
    };

    public static int get() {
        return ((Integer) (serialNum.get())).intValue();
    }
}

Session的管理

经典的另外一个例子:

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  
  

java 开发手册中推荐的 ThreadLocal

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

import java.text.DateFormat;
import java.text.SimpleDateFormat;
 
public class DateUtils {
    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}
  
DateUtils.df.get().format(new Date());

参考文献

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值