精通高并发与多线程,却不会用ThreadLocal?

简介: 本文主要介绍 ThreadLocal 的使用

ThreadLocal 简介


概念


ThreadLocal 类是用来提供线程内部的局部变量。这种变量在多线程环境下访问(getset 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。


ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和上下文。


作用


  • 传递数据


提供线程内部的局部变量。可以通过 ThreadLocal 在同一线程,不同组件中传递公共变量。


  • 线程并发


适用于多线程并发情况下。


  • 线程隔离


每个线程的变量都是独立的,不会相互影响。


ThreadLocal 实战


1. 常见方法


  • ThreadLocal ()


构造方法,创建一个 ThreadLocal 对象


  • void set (T value)


设置当前线程绑定的局部变量


  • T get ()


获取当前线程绑定的局部变量


  • void remove ()


移除当前线程绑定的局部变量


2. 为什么要使用 ThreadLocal


首先我们先看一组并发条件下的代码场景:


@Data
public class ThreadLocalTest {
    private String name;
    public static void main(String[] args) {
        ThreadLocalTest tmp = new ThreadLocalTest();
        for (int i = 0; i < 4; i++) {
            Thread thread = new Thread(() -> {
                tmp.setName(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName() +
                                   "\t 拿到数据:" + tmp.getName());
            });
            thread.setName("Thread-" + i);
            thread.start();
        }
    }
}


我们理想中的代码输出结果应该是这样的:


/** OUTPUT **/
Thread-0   拿到数据:Thread-0
Thread-1   拿到数据:Thread-1
Thread-2   拿到数据:Thread-2
Thread-3   拿到数据:Thread-3


但是实际上输出的结果却是这样的:


/** OUTPUT **/
Thread-0   拿到数据:Thread-1
Thread-3   拿到数据:Thread-3
Thread-1   拿到数据:Thread-1
Thread-2   拿到数据:Thread-2


顺序乱了没有关系,但是我们可以看到 Thread-0 这个线程拿到的值却是 Thread-1

从结果中我们可以看出多个线程在访问同一个变量的时候会出现异常,这是因为线程间的数据没有隔离!


并发线程出现的问题?那加锁不就完事了!这个时候你三下五除二的写下了以下代码:


@Data
public class ThreadLocalTest {
    private String name;
    public static void main(String[] args) {
        ThreadLocalTest tmp = new ThreadLocalTest();
        for (int i = 0; i < 4; i++) {
            Thread thread = new Thread(() -> {
                synchronized (tmp) {
                    tmp.setName(Thread.currentThread().getName());
                    System.out.println(Thread.currentThread().getName() 
                                       + "\t" + tmp.getName());
                }
            });
            thread.setName("Thread-" + i);
            thread.start();
        }
    }
}
/** OUTPUT **/
Thread-2  Thread-2
Thread-3  Thread-3
Thread-1  Thread-1
Thread-0  Thread-0


从结果上看,加锁好像是解决了上述问题,但是 synchronized 常用于多线程数据共享的问题,而非多线程数据隔离的问题。这里使用 synchronized 虽然解决了问题,但是多少有些不合适,并且 synchronized 属于重量级锁,为了实现多线程数据隔离贸然的加上 synchronized,也会影响到性能。


加锁的方法也被否定了,那么该如何解决?不如用 ThreadLocal 牛刀小试一番:


public class ThreadLocalTest {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public String getName() {
        return threadLocal.get();
    }
    public void setName(String name) {
        threadLocal.set(name);
    }
    public static void main(String[] args) {
        ThreadLocalTest tmp = new ThreadLocalTest();
        for (int i = 0; i < 4; i++) {
            Thread thread = new Thread(() -> {
                tmp.setName(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName() + 
                                   "\t 拿到数据:" + tmp.getName());
            });
            thread.setName("Thread-" + i);
            thread.start();
        }
    }
}


在查看输出结果之前,我们先来看看代码发生了那些变化


首先多了一个 private static 修饰的 ThreadLocal ,然后在 setName 的时候,我们实际上是往 ThreadLocal 里面存数据,在 getName 的时候,我们是在 ThreadLocal 里面取数据。感觉操作上也是挺简单的,但是这样真的能做到线程间的数据隔离吗,我们再来看一看结果:


/** OUTPUT **/
Thread-1   拿到数据:Thread-1
Thread-2   拿到数据:Thread-2
Thread-0   拿到数据:Thread-0
Thread-3   拿到数据:Thread-3


从结果上可以看到每个线程都能取到对应的数据。ThreadLocal 也已经解决了多线程之间数据隔离的问题。


那么我们来小结一下,为什么需要使用ThreadLocal,与 synchronized 的区别是什么


  • synchronized


原理: 同步机制采用 "以时间换空间" 的方式,只提供了一份变量,让不同线程排队访问


侧重点: 多个线程之间同步访问资源


  • ThreadLocal


原理: ThreadLocal 采用 "以空间换时间" 的方式,为每个线程都提供了一份变量的副本,从而实现同时访问而互不干扰


侧重点: 多线程中让每个线程之间的数据相互隔离


3. 内部结构


从上面的案例中我们可以看到 ThreadLocal 的两个主要方法分别是 set()get()

那我们不妨猜想一下,如果让我们来设计 ThreadLocal ,我们该如何设计,是否会有这样的想法:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Mapkey,要存储的局部变量作为 Mapvalue ,这样就能达到各个线程的局部变量隔离的效果。


网络异常,图片无法展示
|


这个想法也是没错的,早期的 ThreadLocal 便是这样设计的,但是在 JDK 8 之后便更改了设计,如下:


网络异常,图片无法展示
|


设计过程:


  1. 每个 Thread 线程内部都有一个 ThreadLocalMap


  1. ThreadLocalMap 中存储着以 ThreadLocal 对象为 key ,线程变量为 value


  1. Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 Map 设置和获取线程的变量值


  1. 对于不同的线程,每次获取副本值时,别的线程并不能获取到线程的副本值,这样就会形成副本的隔离,互不干扰


注: 每个线程都要有自己的一个 map,但是这个类就是一个普通的 Java 类,并没有实现 Map 接口,但是具有类似 Map 类似的功能。


网络异常,图片无法展示
|


通过这样实现看起来貌似会比之前我们猜想的更加复杂,这样做的好处是什么呢?


  • 每个 Map 存储的 Entry 数量就会变少,因为之前的存储数量由 Thread 的数量决定,现在是由 ThreadMap 的数量决定,在实际开发中,ThreadLocal 的数量要更少于 Thread 的数量。


  • Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用


4. 源码分析


网络异常,图片无法展示
|


首先我们先看 ThreadLocalMap 中有哪些成员:


网络异常,图片无法展示
|


如果你看过 HashMap 的源码,肯定会觉得这几个特别熟悉,其中:


  • INITIAL_CAPACITY:初始容量,必须是 2 的整次幂


  • table:存放数据的table


  • size:数组中 entries 的个数,用于判断 table 当前使用量是否超过阈值


  • threshold:进行扩容的阈值,表使用量大于它的时候会进行扩容


ThreadLocals


Thread 类中有个类型为 ThreadLocal.ThreadLocalMap 类型的变量 ThreadLocals ,这个就是用来保存每个线程的私有数据。


网络异常,图片无法展示
|


ThreadLocalMap


ThreadLocalMapThreadLocal的内部类,每个数据用Entry保存,其中的Entry用一个键值对存储,键为ThreadLocal的引用。


网络异常,图片无法展示
|


我们可以看到 Entry 继承于WeakReference,这是因为如果是强引用,即使把 ThreadLocal 设置为 nullGC 也不会回收,因为 ThreadLocalMap 对它有强引用。


在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entryEntry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。


网络异常,图片无法展示
|


那是不是就是说如果使用了弱引用,就不会造成内存泄露 呢,这也是不正确的。


因为如果我们没有手动删除 Entry 的情况下,此时 Entry 中的 key == null,这个时候没有任何强引用指向 threaLocal 实例,所以 threadLocal 就可以顺利被 gc 回收,但是 value 不会被回收,而这块的 value 永远不会被访问到,因此会导致内存泄露


网络异常,图片无法展示
|


接下来我们看下 ThreadLocalMap 的几个核心方法:


set 方法


首先我们先看下源码:


public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
        // 存在则调用map.set设置此实体entry
        map.set(this, value);
    else
        // 如果当前线程不存在ThreadLocalMap对象则调用createMap进行ThreadLocalMap对象的初始化
        // 并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    //这里的this是调用此方法的threadLocal
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}


执行流程:


  • 首先获取当前线程,并根据当前线程获取一个 map


  • 如果获取的 map 不为空,则将参数设置到 map 中(当前 ThreadLocal 的引用作为 key


  • 如果 Map 为空,则给该线程创建 map ,并设置初始值


get 方法


源码如下:


public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 如果此map存在
    if (map != null) {
        // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 对e进行判空 
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取存储实体 e 对应的 value值
            // 即为我们想要的当前线程对应此ThreadLocal的值
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
private T setInitialValue() {
    // 调用initialValue获取初始化的值
    // 此方法可以被子类重写, 如果不重写默认返回null
    T value = initialValue();
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
        // 存在则调用map.set设置此实体entry
        map.set(this, value);
    else
        // 如果当前线程不存在ThreadLocalMap对象则调用createMap进行ThreadLocalMap对象的初始化
        // 并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
        createMap(t, value);
    // 返回设置的值value
    return value;
}


执行流程:


  • 首先获取当前线程,根据当前线程获取一个 map


  • 如果获取的 map 不为空,则在 map 中以 ThreadLocal 的引用作为 key 来在 map 中获取对应的 Entry entry  ,否则跳转到第四步


  • 如果 Entry entry 不为空 ,则返回 entry.value ,否则跳转到第四步


  • map 为空或者 entry 为空,则通过 initialValue 函数获取初始值 value ,然后用 ThreadLocal 的引用和 value 作为 firstKeyfirstValue 创建一个新的 map


remove 方法


源码如下:


public void remove() {
    // 获取当前线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 如果此map存在
    if (m != null)
        // 存在则调用map.remove
        m.remove(this);
}
// 以当前ThreadLocal为key删除对应的实体entry
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;
        }
    }
}


执行流程:


  • 首先获取当前线程,并根据当前线程获取一个 map


  • 如果获得的map 不为空,则移除当前 ThreadLocal 对象对应的 entry


initialValue 方法


源码如下:


protected T initialValue() {
    return null;
}


在源码中我们可以看到这个方法仅仅简单的返回了 null ,这个方法是在线程第一次通过 get () 方法访问该线程的 ThreadLocal 时调用的,只有在线程先调用了 set () 方法才不会调用 initialValue () 方法,通常情况下,这个方法最多被调用一次。


如果们想要 ThreadLocal 线程局部变量有一个除 null 以外的初始值,那么就必须通过子类继承ThreadLocal 来重写此方法,可以通过匿名内部类实现。


目录
相关文章
|
5月前
|
存储 监控 安全
解锁ThreadLocal的问题集:如何规避多线程中的坑
解锁ThreadLocal的问题集:如何规避多线程中的坑
205 0
|
5月前
|
存储 Java
高并发编程之多线程锁和Callable&Future 接口
高并发编程之多线程锁和Callable&Future 接口
74 1
|
5月前
|
缓存 监控 Java
高并发编程之ThreadPool 线程池
高并发编程之ThreadPool 线程池
66 1
|
16天前
|
Java Linux
【网络】高并发场景处理:线程池和IO多路复用
【网络】高并发场景处理:线程池和IO多路复用
33 2
|
4月前
|
缓存 NoSQL Java
Java高并发实战:利用线程池和Redis实现高效数据入库
Java高并发实战:利用线程池和Redis实现高效数据入库
456 0
|
1月前
|
消息中间件 安全 大数据
Kafka多线程Consumer是实现高并发数据处理的有效手段之一
【9月更文挑战第2天】Kafka多线程Consumer是实现高并发数据处理的有效手段之一
176 4
|
3月前
|
存储 SQL Java
(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析
在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。
|
3月前
|
安全 Java
多线程线程安全问题之避免ThreadLocal的内存泄漏,如何解决
多线程线程安全问题之避免ThreadLocal的内存泄漏,如何解决
|
3月前
|
存储 安全 Java
多线程线程安全问题之ThreadLocal是什么,它通常用于什么场景
多线程线程安全问题之ThreadLocal是什么,它通常用于什么场景
|
4月前
|
Arthas 监控 Java
深入解析与解决高并发下的线程池死锁问题
在高并发的互联网应用中,遇到线程池死锁问题导致响应延迟和超时。问题源于库存服务的悲观锁策略和线程池配置不当。通过以下方式解决:1) 采用乐观锁(如Spring Data JPA的@Version注解)替换悲观锁,减少线程等待;2) 动态调整线程池参数,如核心线程数、最大线程数和拒绝策略,以适应业务负载变化;3) 实施超时和重试机制,减少资源占用。这些改进提高了系统稳定性和用户体验。
166 2