ThreadLocal深入刨析

一、引入

整篇文章,从以下几个方面来进行梳理

1、使用场景,详细介绍在工作中的使用场景

2、梳理其整体结构,和ThreadLocal相关联的类之间的关系

3、源码解析

  • 构造器
  • set方法
  • get方法
  • remove方法
  • 如何解决hash冲突

5、总结

二、使用场景

2.1、使用

这里给一个简单的demo,来介绍以下ThreadLocal是如何使用的

csharp 复制代码
public class Code2 {


    public static void main(String[] args) {
        //主线程里的值
        local.set("hello");

        new Thread(() -> {
            local.set("Thread one");
            System.out.println(Thread.currentThread().getName() + " : " + local.get());//Thread-0 : Thread one
        }).start();

        new Thread(() -> {
            local.set("Thread two");
            System.out.println(Thread.currentThread().getName() + " : " + local.get());//Thread-1 : Thread two
        }).start();



        System.out.println(Thread.currentThread().getName() + " : " + local.get());//main : hello
        local.remove();
        System.out.println(Thread.currentThread().getName() + " : " + local.get());//main : null

        //用线程池模拟五个线程从ThreadLocal中取值,结果都为Null
        getValue();//结果都为null
    }

    public static final ThreadLocal<String> local = new ThreadLocal<>();

    public static final ExecutorService EXECUTORS = Executors.newFixedThreadPool(5);

    public static void getValue(){
        for (int i = 0; i < 5; i++) {
            EXECUTORS.submit(() -> System.out.println(local.get()));
        }
        EXECUTORS.shutdown();
    }

}

根据结果输出可以看到,使用ThreadLocal做到了线程之间的隔离

2.2、场景一:每个线程需要独享的对象

单独为每一个线程创建一个副本,这样每个线程都可以单独使用自己的副本,做到线程之间的隔离,确保线程安全

在实际工作中,经常用来保存线程不安全的工具类,比如典型的SimpleDateFormat

1、线程安全问题演示:
java 复制代码
public class Code {

    public  static final ExecutorService executor = Executors.newFixedThreadPool(16);
    
    //共用全局对象
    public static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //模拟1000个线程共享一个SimpleDateFormat对象
        for (int i = 0; i < 1000; i++) {
            int f = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(new Code().getDate(f));
                }
            });
        }
        executor.shutdown();
    }


    public String getDate(int timeStamp){
        Date date = new Date(timeStamp * 1000);
        return format.format(date);
    }


}

查看结果很明显出现了重复数据

所有线程都共享一个SimpleDateFormat对象,很容易出现线程安全问题。

2、解决

方案一:每次使用都创建一个SimpleDateFormat对象

java 复制代码
public class Code {

    public  static final ExecutorService executor = Executors.newFixedThreadPool(16);


    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int f = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(new Code().getDate(f));
                }
            });
        }
        executor.shutdown();
    }


    public String getDate(int timeStamp){
        Date date = new Date(timeStamp * 1000);
        //给每一个线程创建一个SimpleDateFormat对象不会有线程安全问题
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return format.format(date);
    }


}

这种方案是让每一个线程都会创建一个SimpleDateFormat对象,这么多对象的创建是有开销的,并且对象的销毁也是有开销的,这么多重复对象存在于内存中,也是对内存的浪费,如果使用不当,容易造成GC频繁,不推荐使用,建议使用方案二。

方案二:使用ThreadLocal,让每个线程独享一个SimpleDateFormat对象

csharp 复制代码
public class SimpleDateFormatUtils {

    private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };


    public static SimpleDateFormat getFormat() {
        SimpleDateFormat simpleDateFormat = THREAD_LOCAL.get();
        if(simpleDateFormat == null){
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
        return simpleDateFormat;
    }

}

2.3、场景二:每个线程内需要保存的信息

在生产环境中,本人使用到的ThreadLocal的场景

  • 登录成功后,存放用户的完整信息
  • 灰度发布时,存放服务的版本信息

每个线程获取到的登录信息都是不一样的,用户登录,在拦截器里进行校验通过之后,直接将后续流程需要用到的信息存放到ThreadLocal中,后续方法就能直接从ThreadLocal中直接获取,避免了传参

  • 登录成功后,存放用户的完整信息【权限信息+用户信息】

无需任何措施就能保证线程安全,每个线程都有自己独立的信息,可以做到线程隔离

  • 做灰度发布时,根据版本号来走到对应的版本信息

三、整体结构

ThreadThreadLocalThreadLocalMap三者的整体关系

演化一下:

每一个Thread线程都会对应一个ThreadLocalMap,底层是Entry类型的数组,Entry对象里有两个属性,分别对应key和value,key是当前ThreadLocal对象,并且是个弱引用,value是Object类型的强引用。

key是弱引用代表这只有有 GC 就会被回收

key被回收了,但是value还可能会存在,容易造成内存泄露

这就是为什么,必须使用完ThreadLocal,就一定要进行remove

四、源码分析

4.1、构造器

默认一个空构造器,初始化了一个ThreadLocal对象

csharp 复制代码
// 默认的空构造器
public ThreadLocal() {
}

4.2、Set方法

scss 复制代码
public void set(T value) {
    // 从线程里取出当前线程
    Thread t = Thread.currentThread();
    // 从线程里取出ThreadLocalMap,头一次取出来的基本都是空
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

// 每一个线程都有一个属性参数 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

第一次线程进来时,ThreadLocalMap为空

执行完Set方法后,会给对应的线程的threadLocal属性里面进行初始化

ini 复制代码
private void set(ThreadLocal<?> key, Object value) {

    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)]) {

        // 找到了相同的key 替换value
        if (e.refersTo(key)) {
            e.value = value;
            return;
        }

        // 清理过期条目
        if (e.refersTo(null)) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 新增key,value
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 下一个索引位置 +1 , 到达末尾回到 0
private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

4.3、get方法

kotlin 复制代码
public T get() {
    // 传入当前线程的值
    return get(Thread.currentThread());
}

private T get(Thread t) {
    // 拿到当前线程的Map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // map里面封装值用的 Entry对象,key就是当前的ThreadLocal
        // 从这里就可以判断出,一个 ThreadLocalMap里只维护一个entry -> 言外之意就是只能有一个key-val对
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 取出当前线程里的值,返回 
            T result = (T) e.value;
            return result;
        }
    }
    // 如果map是空的,初始化map
    return setInitialValue(t);
}



  // 拿到当前线程ThreadLocalMap
  ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
  }

  /*
   * 每个 Thread都会维护一个 ThreadLocalMap
   */
  ThreadLocal.ThreadLocalMap threadLocals;

从每个线程的 ThreadLocalMap里取出Entry对象,从 Entry对象里取出对应的值

4.4、remove方法

ini 复制代码
public void remove() {
     // 拿到当前线程
     remove(Thread.currentThread());
}

private void remove(Thread t) {
     // 从线程里取出 ThreadLocalMap
     ThreadLocalMap m = getMap(t);
     if (m != null) {
         // key就是当前 ThreadLocal,根据key从map里移除掉val
         m.remove(this);
     }
 }

private void remove(ThreadLocal<?> key) {
        // 拿到Entry数组对象
        Entry[] tab = table;
        int len = tab.length;
        // 根据key计算出具体在数组里的位置
        int i = key.threadLocalHashCode & (len-1);
        // 这里为什么要进行遍历?为了解决hash冲突,如果计算出来的i 出现 hash冲突,不是对应的key
        // 从index = i 开始,依次往后查找,直到找到对应的key
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.refersTo(key)) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }

五、总结

ThreadLocal提供了线程局部变量,使得每个线程都有自己独立的副本,解决了多线程环境下共享变量的安全问题。

  • Thread内部维护一个 ThreadLocalMap来存储ThreadLocal变量
  • 每个线程都有自己独立的 ThreadLocalMap
  • key是 ThreadLocal实例,val是存储的值

ThreadLocal解决hash冲突的方式:

  • 利用数组,冲突比对key,一样就替换,不一样继续下一个,直到线性探测完所有元素
相关推荐
IMPYLH2 小时前
Lua 的 IO (输入/输出)模块
开发语言·笔记·后端·lua
爱可生开源社区2 小时前
SCALE | SQLFlash 在 SQL 优化维度上的表现评估
后端
程序员小假2 小时前
我们来说一下消息的可靠性投递
java·后端
duangww3 小时前
SAPUI5 1.71.78老版本的消费restful服务
后端·restful
用户8599681677693 小时前
UE5虚幻引擎汽车HMI设计高级研修课
后端
用户8599681677693 小时前
鸿蒙HarmonyOS多线程编程实战:AI语音
后端
开心猴爷3 小时前
iOS 应用发布流程中常被忽视的关键环节
后端
用户21991679703913 小时前
使用Agent Framework进行多Agent工作流编排
后端
serendipity_hky3 小时前
【go语言 | 第5篇】channel——多个goroutine之间通信
开发语言·后端·golang