浅聊 【ThreadLocal】(超级详细)

写在开始 :
本文主要讲述 : ThreadLocal简介; 常用API; demo案例; 特点引用场景;以及部分底层原理源码内容。

引言 :

从常见面试题看 ThreadLocal:
**①解释 **: ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分
配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时
实现了线程内的资源共享;
②案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的
ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操
作,避免A线程关闭了B线程的连接。

public class ThreadLocalTest {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    public static void main(String[] args) {
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("itcast"); // set(v)方法 : 设置值
            print(name);
            System.out.println(name + "-after remove : " +
            threadLocal.get());        // get()方法:获取值
        }, "t1").start();
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("itheima");
            print(name);
            System.out.println(name + "-after remove : " +
            threadLocal.get());
        }, "t2").start();
    }
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + threadLocal.get());
        //清除本地内存中的本地变量
        threadLocal.remove();         // remove()方法 清除值
    }
}

③源码
ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内
部的值,从而实现线程数据隔离;
image.png
在ThreadLocal中有一个内部类叫做ThreadLocalMap,类似于HashMap
ThreadLocalMap中有一个属性table数组,这个是真正存储数据的位置
Ⅰ:set方法:
image.png
Ⅱ:get/remove方法
image.png

内存泄漏问题
每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承
了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量
的副本;
引用类型 : ①弱引用:内存不太够时候优先回收;②强引用:不会被回收
使用 ThreadLocal 时候 ,强烈建议:务必手动remove。

正文第一部分–编程实战

1.ThreadLocal 简介

ThreadLocal 见名知意 : 线程本地 或者 本次线程;
但是从它实际作用而言, 可能解释为 【线程局部变量】or 【线程本地变量】更为合适。

2.ThreadLocal作用

ThreadLocal是java.lang包中的一个泛型类,可以实现为线程创建独有变量,这个变量对于其他线程是隔离的,也就是线程本地的值,这也是ThreadLocal名字的来源;

package java.lang

public class ThreadLocal<T>{}

每个使用该变量的线程都要初始化一个完全独立的实例副本(也就是变量的本地拷贝),不存在多线程间共享问题

3.ThreadLocal的 API 方法

ThreadLocal用来存储当前线程的独有数据,相关API就是存值,取值,清空值的简单操作

  • withInitial:创建一个ThreadLocal实例,并给定初始值【JDK8推出的新方法,一般都是用该方法初始化ThreadLocal】
  • get:返回当前线程ThreadLocal的值,如果没有设置值返回null
  • set:设置当前线程ThreadLocal的值
  • remove:删除当前线程ThreadLocal的值
  • initialValue:此方法默认返回null, 通过ThreadLocal构造方法初始化时一般重写此方法,来设置初始值,在JDK8之后通过withInitial方法初始化

4.初始化ThreadLocal

初始化ThreadLocal有三种方式:

  • 直接通过构造方法创建,此时初始值为null
  • 通过构造方法同时重写initialValue方法给定初始值
  • 通过JDK8的withInitial()静态方法创建,可以通过Lambda直接给初始值【推荐使用】

①构造方法

@Test
public void test1() {
    // 通过构造方法,初始化ThreadLocal
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    // 未给定初始值,通过get方法获取值为null
    System.out.println(threadLocal.get());
}


@Test
public void test2() {
    // 初始化ThreadLocal
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    System.out.println("设置前  -->" + threadLocal.get()); // null
    // 通过 set 方法,设置初始值
    threadLocal.set(1);
    System.out.println("设置后  -->" + threadLocal.get()); // 1
}

②重写initialValue方法

@Test
public void test3() {
    // 初始化ThreadLocal,重写initialValue方法设置默认值
    ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            // 设置,初始值为1024
            return 1024;
        }
    };
    
    // 启动5个线程
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":threadLocal初始值 -->" + threadLocal.get());
        },"线程:" + i).start();
    }
}

打印结果 : 5个线程每个线程的初始值都为1024

// 返回当前线程ThreadLocal的初始值,但是在ThreadLocal中默认实现返回为null
// 这个值和具体的泛型类型有关,通常需要根据实际需求重写此方法,定义初始值

protected T initialValue(){
    return null;
}

③withInitial静态方法

// JDK8中新增了withInitial静态方法接收Supplier供给型函数接口设置初始值
@Test
public void test4() {
    // 通过withInitial方法设置初始值
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> {
        return 100;
    });
    // 启动5个线程
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":threadLocal初始值 -->" + threadLocal.get());
        },"线程:" + i).start();
    }
}

打印结果 :
启动5个线程之后初始值为100,
强烈推荐此种方式来实现ThreadLocal的初始化
推荐通过withInitial静态方法实现ThreadLocal的初始化
如果线程内需要修改值则可以使用set方法,如果需要获取值则使用get方法
ThreadLocal数据存储:一个ThreadLocal实例,在每个线程中都有独自的初始化副本,接下来每个线程对ThradLocal的操作都在线程内,对其他线程隔离

5.demo案例

背景说明:

@Test
public void demo() {
    // 通过withInitial方法设置初始账户余额为0
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    
    // 设置随机数
    Random random = new Random();
    
    // 通过map集合映射线程名字
    Map<Integer, String> map = new HashMap<>();
    map.put(0,"账户一");
    map.put(1,"账户二");
    map.put(2,"账户三");
    
    // 启动3个线程,分别为3个账户
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            // 去存钱5次
            for (int j = 0; j < 5; j++) {
                // 每次随机,金额在200以内
                int money = random.nextInt(200);
                
                // 金额加1
                threadLocal.set(money + threadLocal.get());
            }
            System.out.println(Thread.currentThread().getName() + "共存入:" + threadLocal.get() + "元的钱");
        },map.get(i)).start();
    }
}

三个账户各自存款五次,金额实现单独记录
打印结果 : 账户一共存入:499元; 账户二共存入:528元; 账户三共存入:799元;

线程池实现

实际开发对于多线程的场景都推荐使用线程池实现,可以避免线程频繁创建和销毁的资源浪费,使用线程池是一定要记得在finally代码块中关闭线程池

@Test
public void test6() {
    // 通过withInitial方法设置初始值
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    // 设置随机数
    Random random = new Random();

    Map<Integer, String> map = new HashMap<>();
     map.put(0,"账户一");
    map.put(1,"账户二");
    map.put(2,"账户三");

    // 开启有3个核心线程的线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(3);
    try {
        // 3个账户
        for (int i = 0; i < 3; i++) {
            // 记录当前循环值,映射对应的账户
            int index = i;
            threadPool.submit(() -> {
                // 设置线程名:Thread.currentThread().getName()为默认的线程池给的名字,方便查看是哪一个线程执行的此任务
                Thread.currentThread().setName(Thread.currentThread().getName() + map.get(index));
                // 存钱,比如5次
                for (int j = 0; j < 5; j++) {
                    // 每家随机,金额200以内
                    int money = random.nextInt(200);
                    // 金额加1
                    threadLocal.set(money + threadLocal.get());
                }
                System.out.println(Thread.currentThread().getName() + "共:" + threadLocal.get() + "元");
            });
        }
    }catch (Exception e) {
        e.printStackTrace();
    }finally {
        // 关闭线程池
        threadPool.shutdown();
    }
}

假设运行程序的电脑为8核,可以通过线程池中的3个核心线程,同时执行3条任务,此时的结果没有任何问题
打印结果:
pool-1-thread - 1 账户一共:477元
pool-1-thread - 3 账户三共:777元
pool-1-thread - 2 账户二共:557元

线程池中线程可复用引发的问题

如果此时又多出来一个账户,或者核心线程数变为2,即任务数大于核心线程数,会复用线程处理其他任务,即一个线程需要处理多个任务,这里减少核心线程数为例演示:

@Test
public void test6() {
    ......
    // 修改线程池核心线程数为2,其他不变
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    ......
}

**原因在于:**1号线程处理完账户一之后,发现账户二任务没有执行,此时1号线程处理两个任务,ThreadLocal也还是同一个,即处理账户二任务时,ThreadLocal的值为账户一任务处理后的值,并不是初始值0;
在阿里Java开发规范中强制要求回收自定义的ThreadLocal变量

通过try块将任务逻辑包裹,在finally中通过remove方法回收该任务执行后的ThreadLocal值

@Test
public void test7() {
    ......
    try {
        ......
            try {
                for (int j = 0; j < 5; j++) {
                    // 随机的200以内
                    int money = random.nextInt(200);
                    // 金额加1
                    threadLocal.set(money + threadLocal.get());
                }
                System.out.println(Thread.currentThread().getName() + "共:" + threadLocal.get() + "元");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 回收数据
                threadLocal.remove();
            }
    }catch (Exception e) {
        e.printStackTrace();
    }finally {
        // 关闭线程池
        threadPool.shutdown();
    }
}

回收ThreadLocal变量后,复用该线程也不会对后续程序造成影响

6.ThreadLocal特点

  • 统一设置初始值,每个线程可以通过set方法设置值,也可以通过get方法获取当前值
  • ThreadLocal被每一个线程单独持有副本,相互独立,只能在该线程内部使用
  • 如果配合线程池使用,线程可复用,需要调用remove方法回收数据,即重新设置为初始值,避免对后续程序造成影响和内存泄漏
  • ThreadLocal变量因为线程独立,所以不在线程安全问题

7.ThreadLocal应用场景

ThreadLocal 适用于如下两种场景

  • 每个线程需要自己独立的数据
  • 数据在线程内的共享,不需要在多线程之间共享

举例:

  • 游戏玩家个人的属性,装备,积分等
  • Spring中也通过ThreadLocal解决线程安全问题,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的;

正文第二部分–底层原理

1.ThreadLocal线程安全原理

ThreadLocal存储数据是通过ThreadLocalMap实现,ThreadLocalMap底层是Entry数组,以键值对的形式存放,键是ThreadLocal对象,value是set()设置的值

  • 当执行set方法时:其value是保存在threadlocals变量中的
  • 当执行get方法时:就是读取threadlocals变量中的值。
  • 各个线程的数据互不打扰。一个线程可以存放多个ThreadLocal变量,他们都是存在了ThreadLocalMap中
/* 记录与此线程相关的ThreadLocal值。这个map由ThreadLocal类维护 */
ThreadLocal.ThreadLocalMap threadLocals = null;

首先,在Java中的线程是一个Thread类的实例对象!而一个实例对象中实例成员字段的内容肯定是这个对象独有的,所以如果将ThreadLocal线程本地变量作为Thread类的成员字段,就可实现线程私有,在Thread类中恰巧就是这么实现的,这个成员字段就是上述代码

map是一个在ThreadLocal中通过静态内部类定义的Map对象,保存了该线程中的所有本地变量【即一个线程可以使用多个ThreadLocal,存储在指定Thread的Map中】。ThreadLocalMap中的Entry的定义如下:

static class ThreadLocalMap{   // 静态内部类
    static class Entry extends WeakReference<ThreadLocal<?>>{ // 通过Entry对象维护数据,继承弱引用
        Object value;

        Entry(ThreadLocal<?> k,Object v){
            super(k); // 弱引用,内存不够优先回收
            value = v; // 强引用,不会回收
        }
    }
}
  • 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
  • ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
  • 我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap
  • ThreadLocalMap是ThreadLocal类中的静态内部类,通过内部类Entry记录值,
  • 在ThreadLocalMap中维护这一个Entry数组,因为一个线程可以使用多个ThreadLocal变量
  • Entry将该Thread中使用的ThreadLocal当做key,将值当做value【实际上key并不是ThreadLocal本身,而是它的一个弱引用】。
  • ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value

Set源码

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的threadLocals字段
    ThreadLocalMap map = getMap(t);
    // 判断线程的threadLocals是否初始化了
    if (map != null) {
        map.set(this, value);
    } else {
        // 没有则创建一个ThreadLocalMap对象进行初始化
        createMap(t, value);
    }
}

createMap方法的源码

void createMap(Thread t, T firstValue) {
    // 新建一个ThreadLocalMap
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

map.set方法的源码

/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
    // map中就是使用Entry[]数据保留所有的entry实例
    Entry[] tab = table;
    int len = tab.length;
    // 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关
    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();
}

get方法原理

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 获取ThreadLocal对应保留在Map中的Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取ThreadLocal对象对应的值
            T result = (T)e.value;
            return result;
        }
    }
    // map还没有初始化时创建map对象,并设置null,同时返回null
    return setInitialValue();
}

remove方法原理

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 键在直接移除
    if (m != null) {
        m.remove(this);
    }
}

2.ThreadLocal设计

JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal,value是存储的是泛型值,其具体过程如下:

  • 每个Thread线程内部都有一个Map【ThreadLocalMap.threadlocals】;
  • Map里面存储ThreadLocal对象【key】和线程的变量副本【value】;
  • Thread内部Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;
  • 对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。

这样设计的好处是:

  • 每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突;
  • 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。

使用ThreadLocal的好处

  • 保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;
  • 各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。

3.ThreadLocal内存泄露问题

**内存泄露问题:**指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出。

ThreadLocal的内存泄露问题一般考虑和Entry对象有关,Entry继承WeakReference也就是弱引用。**JVM会将弱引用修饰的对象在下次垃圾回收中清除掉。**这样就可以实现ThreadLocal的生命周期和线程的生命周期解绑。但实际上并不是使用了弱引用就会发生内存泄露问题,考虑下面几个过程:

强引用


当ThreadLocal Ref被回收了,由于Entry使用的是强引用,在Current Thread还存在的情况下就存在着到达Entry的引用链,无法清除掉ThreadLocal的内容,同时Entry的value也同样会被保留;也就是说就算使用了强引用仍然会出现内存泄露问题。

弱引用


当ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的时候就会将ThreadLocal对象清除,这个时候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄露的问题。

综上所述,使用ThreadLocal造成内存泄露的问题是因为:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄露问题。因此,需要我们在每次在使用完之后需要手动的remove掉Entry对象。

避免内存泄露的两种方式:使用完ThreadLocal,调用其remove方法删除对应的Entry或者使用完ThreadLocal,当前Thread也随之运行结束。第二种方法在使用线程池技术时是不可以实现的。
所以一般都是自己手动调用remove方法,调用remove方法弱引用和强引用都不会产生内存泄露问题,使用弱引用的原因如下:
在ThreadLocalMap的set/getEntry中,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!
:::info
TIP:
存在内存泄露的有两个地方:ThreadLocal和Entry中Value;最保险还是要注意要自己及时调用remove方法!!
:::

补充面试题部分

问题一:实际开发有用过ThreadLocal吗?

举例1 : 用来做用户信息上下文存储;
当系统应用是MVC架构的背景,用户每次登录访问接口,都会在请求头携带一个 token,控制层可以根据这个 token ,解析用户基本信息,那么在服务层,持久层也要用户信息,比如 rpc调用,更新用户获取等等,就可以采取如下两种方式:
方式一: 显式定义用户相关参数,比如账户,用户名等等,但是这样会大面积修改代码。这时候用 ThreadLocal ,控制层拦截请求把用户信息存入ThreadLocal,就能在需要的地方,取出ThreadLocal里存的用户数据;

其它有些场景的 cookie session 等数据隔离也可以通过 ThreadLocal 去实现;
之前引言案例举例的数据库连接池也用到 ThreadLcoal:
DB连接池的连接交给ThreadLocal进行管理,保证当前线程的操作都是一个Connection;

问题二:谈谈你对ThreadLocal的理解?

ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现 了线程内的资源共享

问题三:ThreadLocal的底层原理实现?

①在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来 存储资源对象
②当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
③当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联 的资源值
④当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联 的资源值

问题四:ThreadLocal内存溢出?

  1. 因为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释 放key,不过关键的是只有key可以得到内存释放,而value不会,因为value 是一个强引用。
  2. 在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依 靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。

小结

:::info

  • ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在线程之间隔离和当前线程全局共享。
  • 线程的隔离性和变量的线程全局共享性得益于在每个Thread类中的threadlocals字段。(从类实例对象的角度抽象的去看Java中的线程!!!)
  • ThreadLocalMap中Entry的Key不管是否使用弱引用都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!

:::

写在最后 : 码字不易,如果觉得还不错,或者对您有帮助,麻烦动动小手,点赞或关注,谢谢! 祝大家周末愉快!