首页 > 其他分享 >深入理解ThreadLocal

深入理解ThreadLocal

时间:2022-11-24 22:32:58浏览次数:61  
标签:static return ThreadLocal 理解 线程 new 深入 public


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 全栈知识体系​

标签:static,return,ThreadLocal,理解,线程,new,深入,public
From: https://blog.51cto.com/u_15890333/5885134

相关文章