一篇文章看懂ThreadLocal!

ThreadLocal(线程本地存储模式)

  • 多线程之所以让人头疼就是其对于临界区即共享变量的改变是难以预计的。
    但正所谓没有买卖就没有伤害,在多线程世界里没有共享就没有伤害。局部变量存储在栈里,栈是线程私有的,所以局部变量就不存在多线程的安全性问题,那么除此之外还有没有是线程安全的存储方式呢。

  • 当然有,那就是ThreadLocal即线程本地存储。spring的事务控制动态数据源自定义注解的数据源切换都用到了ThreadLocal,那我们就来看看ThreadLocal到底是为什么可以做到线程隔离。

 ThreadLocal threadLocal = new ThreadLocal();

  public void fun(String[] args) {
        threadLocal.set("123");
        final Object o = threadLocal.get();
    }

大家都知道ThreadLocal对象拥有两个核心成员方法,set和get,同一个线程内使用同一个ThreadLocal对象set一个值,就能get出这个值,set第二遍就会覆盖上一个值。不同的线程不会get到其他线程set的数据。到底是什么样的数据结构可以让成员变量threadLocal保证线程之间不会互相影响的呢。

  • 我们思考下,线程都有自己的一个唯一Id,我们假设将id作为key,set(value)的实际作用是set(currentThreadId,value),而get()的实际实现是get(currentThreadId),这样子ThreadLocal内部就维护了一个Map表,如下图所示
    在这里插入图片描述
  • 它的核心方法实现是这样的,如下代码所示
 	/**
 * @author pp_x
 * @email pp_x12138@163.com
 * @Description
 * @date 2022/11/20 19:43
 */

public class MyThreadLocal<T> {
    Map<Thread, T> locals =
            new ConcurrentHashMap<>();

    // 获取线程变量
    T get() {
        return locals.get(
                Thread.currentThread());
    }

    // 设置线程变量
    void set(T t) {
        locals.put(
                Thread.currentThread(), t);
    }
}

这样子,将线程唯一id作为map的key,就完全可以实现不同线程存的值不会相互影响了,完美!收工!
在这里插入图片描述
jdk内部的ThreadLocal实现真的是这样的吗,当然不是,那么到底是什么样的呢,点进源码不就知道了,下面就是ThreadLocal在jdk8下的get方法和set方法的实现

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();
    }

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

根据源码可以看到,方法内部获取了类型为ThreadLocalMap的变量,但是不同的是这个map的key并不是线程id而是当前ThreadLocal对象(this)。这也就证明我们的思考是错误滴,下面是jdk对于ThreadLocal的实现概念图,这边贴出来便于大家进行对比
在这里插入图片描述

  • 其实再往下看源码就会发现ThreadLocalMap这个变量并不是ThreadLocal内部持有的,而是Thread内部持有的,这是上面getMap()的内部实现,我们就能看出ThreadLocalMap的从属关系。
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  • 那么这么也就是说ThreadLocal其实只是一个代理类,内部并不持有任何与线程相关的数据,所有和线程
    相关的数据都存储在 Thread 里面。其实这样才是最合理的。当然这样设计的原因就是,不容易产生内存泄漏
  • 我们回到我们上面的思考的模型,图再贴一下方便查看
    在这里插入图片描述
  • 每有一个线程调用set方法,就会往ThreadLocal内部的map里插入一条数据,而map是ThreadLocal的强引用,只要ThreadLocal对象存在,map就永远不会被回收,即使线程销毁了,map的内存空间依然不会释放,那么大量线程频繁调用set,自然就产生内存泄漏了。

在这里插入图片描述

  • 反观jdk内部的结构,我们会发现ThreadLocalMap是ThreadLocal的静态内部类,同时也是Thread的成员变量。我们每使用一次threadLocal.set(value),就会获取当前线程内部的ThreadLocalMap,并将当前threadLocal的对象引用作为key存入map,这样的好处是,ThreadLocalMap的生命周期和Thread强关联,并且ThreadLocalMap里对于ThreadLocal的引用还是弱引用(WeakReference),当线程销毁后map的空间自然释放了。

ThreadLocalMap的内部实现

  • 通过上面ThreadLocalMap的代码可以看到,其内部还持有一个Entry的静态内部类,继承了ThreadLocal的弱引用,Entry仅拥有属性value,通过Entry的构造方法可以看出,Entry中的这个value即存放ThreadLocal.set(value)的value值。
  • ThreadLocalMap中持有Entry类的数组对象table,table内存存放的就是同一个线程使用多个ThreadLocal对象set进来的值。

ThreadLocal的核心方法调用流程

Set方法调用流程

  • 调用ThreadLocal对象的set后,通过Thread类的currentThread()方法获取当前线程,并获取当前线程的ThreadLocalMap对象,再调用ThreadLocalMap对象的set方法,下方是ThreadLocalMap的set方法的实现具体逻辑可以自行翻阅源码
  • 大致是根据ThreadLocal引用key的hash和数组长度确定当前索引,取出索引对应的Entry,再获取Entry的ThreadLocal的引用key,最后做一系列判断操作进行value的赋值
  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();
        }

get方法调用流程

  • 调用ThreadLocal对象的get后,同set一样最后调用ThreadLocalMap的getEntry方法,取出Entry内部的value对象。下面是ThreadLocalMap内部的genEntry方法实现
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后的理解,如有不对,希望可以如数指出!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
ThreadLocalJava中一个非常重要的线程封闭技术。它可以让每个线程都拥有自己的变量副本,避免了线程间的竞争和数据泄露问题。在本文中,我们将详细介绍ThreadLocal的定义、用法及其优点。 1. ThreadLocal的定义 ThreadLocalJava中一个用来实现线程封闭技术的类。它提供了一个本地线程变量,可以在多线程环境下使每个线程都拥有自己的变量副本。每个线程都可以独立地改变自己的副本,而不会影响到其他线程的副本。ThreadLocal的实现是基于ThreadLocalMap的,每个ThreadLocal对象都对应一个ThreadLocalMap,其中存储了线程本地变量的值。 2. ThreadLocal的用法 使用ThreadLocal非常简单,只需要创建一个ThreadLocal对象,然后调用其get()和set()方法即可。get()方法用来获取当前线程的变量副本,如果当前线程还没有变量副本,则会创建一个新的副本并返回。set()方法用来设置当前线程的变量副本,如果当前线程已经有了变量副本,则会覆盖原来的副本。 下面是一个简单的例子,演示了如何使用ThreadLocal来实现线程封闭: ```java public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { new Thread(() -> { threadLocal.set("Thread A"); System.out.println("Thread A: " + threadLocal.get()); }).start(); new Thread(() -> { threadLocal.set("Thread B"); System.out.println("Thread B: " + threadLocal.get()); }).start(); Thread.sleep(1000); System.out.println("Main: " + threadLocal.get()); } } ``` 运行结果如下: ``` Thread A: Thread A Thread B: Thread B Main: null ``` 从输出结果可以看出,每个线程都拥有自己的变量副本,互不影响。而在主线程中,由于没有设置过变量副本,所以返回null。 3. ThreadLocal的优点 ThreadLocal的优点主要体现在以下几个方面: (1)线程安全:ThreadLocal可以避免线程间的竞争和数据泄露问题,每个线程都可以独立地修改自己的变量副本,不会影响其他线程。 (2)高效性:ThreadLocal使用起来非常简单,而且性能也非常高,比如在Web开发中,可以将用户信息存储在ThreadLocal中,从而避免在每个方法中都去查询数据库。 (3)易用性:ThreadLocal的使用非常灵活,可以根据实际需要自由地定义数据类型和访问方式。 总的来说,ThreadLocalJava中一个非常重要的线程封闭技术,可以帮助开发人员避免线程间的竞争和数据泄露问题,提高程序的安全性和性能。在实际开发中,我们应该充分利用ThreadLocal的优点,合理地运用它来解决各种线程安全问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值