ThreadLocal详解

Posted 热爱Java,热爱生活

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal详解相关的知识,希望对你有一定的参考价值。

ThreadLocal 是 Java 中的一个线程本地变量工具类,它可以用来在每个线程中存储特定的数据,而不需要担心线程安全问题。

当我们创建一个 ThreadLocal 变量时,每个线程都会创建该变量的副本,并且每个线程都只能访问自己的副本,而不会干扰其他线程的副本。这使得 ThreadLocal 变量在多线程环境中非常有用,因为它可以避免多个线程之间共享数据时的竞态条件和锁等问题。

使用 ThreadLocal 的基本流程如下:

  1. 创建 ThreadLocal 变量。

  2. 在每个线程中使用 get() 方法获取该变量的副本,并对副本进行读写操作。

  3. 在线程结束时,通过调用 remove() 方法清除该变量在当前线程中的副本。

以下是一个简单的示例:

public class Example 
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) 
        threadLocal.set("Hello, ThreadLocal!");
        new Thread(() -> 
            threadLocal.set("Hello, World!");
            System.out.println("Thread 1: " + threadLocal.get());
        ).start();
        new Thread(() -> 
            threadLocal.set("Goodbye, Cruel World!");
            System.out.println("Thread 2: " + threadLocal.get());
        ).start();
        System.out.println("Main thread: " + threadLocal.get());
        threadLocal.remove();
    

输出结果:

Main thread: Hello, ThreadLocal! 
Thread 1: Hello, World! 
Thread 2: Goodbye, Cruel World!

可以看到,在主线程中,threadLocal.get() 返回的是初始值 "Hello, ThreadLocal!",而在两个新线程中,threadLocal.get() 分别返回了它们各自设置的值。此外,注意在每个线程结束时,我们调用了 threadLocal.remove() 方法,以确保清除该变量在当前线程中的副本。

虽然 ThreadLocal 变量不需要显式地进行同步操作,但它也不是绝对线程安全的。如果多个线程共享同一个 ThreadLocal 变量,并且每个线程都对它进行写操作,则需要考虑同步问题。此外,使用 ThreadLocal 也可能会导致内存泄漏问题,因为每个线程都会持有该变量的副本,如果副本没有及时清除,则可能会导致内存占用过高。

下面是ThreadLocal的一些详解:

  1. ThreadLocal的作用 ThreadLocal主要用于解决多线程并发访问时,对于每个线程都要独立地保存变量的需求。在多线程环境中,如果共享一个变量,容易出现数据竞争、线程安全等问题。而使用ThreadLocal可以使每个线程都拥有自己的变量副本,从而避免这些问题。

  2. ThreadLocal的实现原理 ThreadLocal的实现原理是通过为每个线程都创建一个独立的变量副本来解决多线程并发访问时的问题。每个线程都可以通过get()方法获取自己的变量副本,并通过set()方法设置自己的变量副本。在多线程环境下,每个线程都是独立的,所以每个线程都可以访问自己的变量副本,而不会影响其他线程的变量副本。

  3. ThreadLocal的使用方式 ThreadLocal的使用方式比较简单,通常是通过静态方法ThreadLocal.withInitial()来创建一个ThreadLocal对象,然后使用get()和set()方法来获取和设置当前线程的变量副本。

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Hello, World");
String value = threadLocal.get();
threadLocal.set("Hello, ThreadLocal");

  1. ThreadLocal的注意事项 在使用ThreadLocal时需要注意以下几点:
  • 确保每个线程都能独立地访问自己的变量副本。
  • 变量副本的生命周期应该与线程的生命周期相同。
  • 在使用完ThreadLocal变量后,应该及时清除它,避免内存泄漏问题。
  1. ThreadLocal的优缺点 ThreadLocal的优点是可以避免多线程并发访问时出现的数据竞争、线程安全等问题。同时,使用ThreadLocal也可以使代码更加简洁,不需要额外处理线程间的数据共享问题。ThreadLocal的缺点是在使用过程中需要注意清除变量副本,否则可能会出现内存泄漏问题。同时,在使用ThreadLocal时,需要注意控制变量副本的生命周期,避免不必要的资源浪费。

ThreadLocal 详解

一、ThreadLocal简介
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

下图可以增强理解:

ThreadLocal

二、ThreadLocal与Synchronized的区别
ThreadLocal<T>其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。

但是ThreadLocal与synchronized有本质的区别:

1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

2、Synchronized是利用锁的机制,使变量或代码块在某一时刻只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本

,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

三、ThreadLocal的原理

要看原理那么就得从源码看起。

public void set(T value) 
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
//则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化thradLocalMap 并赋值
createMap(t, value);

从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。

static class ThreadLocalMap 

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>>
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v)
super(k);
value = v;




可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用(发生GC即被回收)。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue)
t.threadLocals = new ThreadLocalMap(this, firstValue);



//ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);

 3.2 ThreadLocal的get方法

public T get() 
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map数据为空,
if (map != null)
//3.1、获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;


//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return 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;

3.3 ThreadLocal的remove方法

public void remove() 
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);

remove方法,直接将ThrealLocal 对应的值从当前Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。(如下图,虚线是弱引用而实线是强引用)

ThreadLocal

ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

四、ThreadLocal 常见使用场景

如上文所述,ThreadLocal 适用于如下两种场景

  • 1、每个线程需要有自己单独的实例
  • 2、实例需要在多个方法中共享,但不希望被多线程共享

场景

1)存储用户Session(项目中用到)

一个简单的用ThreadLocal来存储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;

在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。

在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)

对于笔者而言,这个场景使用的比较多,当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空(这一点很重要会产生内存泄漏), 中间的过程无需在关注如何获取用户信息,只需要使用工具类的get方法即可。

场景二、数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。

场景四、Spring使用ThreadLocal解决线程安全问题

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示。

ThreadLocal

 这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

ref:​​(74条消息) 史上最全ThreadLocal 详解(一)_FMcGee的博客-博客_threadlocal​

​(74条消息) ThreadLocal 常见使用场景_lsz冲呀的博客-博客_threadlocal实际应用场景​



以上是关于ThreadLocal详解的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal 详解

Java 并发详解 ThreadLocal

Java并发系列03ThreadLocal详解

java之ThreadLocal详解

Java中的ThreadLocal详解

java中的ThreadLocal详解及示例代码