栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

ThreadLocal的使用和底层源码解读

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

ThreadLocal的使用和底层源码解读

目录

前言

正文

ThreadLocal的使用

ThreadLocal的作用

ThreadLocal底层源码解读

提出疑问

能否使用HashMap来替代ThreadLocal?

ThreadLocal的get()方法

ThreadLocal的set()方法

如何解决内存泄漏

总结

课程视频推荐(免费)


前言

目前也是金三银四跳槽好时机,各位小伙伴可能都在面临着不同的面试。对于Java后端的小伙伴来说多线程方面的问题肯定必问的。比如你Java的多线程有了解吗?有了解是吧,那你知道ThreadLocal吗?项目中有使用过ThreadLocal吗?ThreadLocal他的作用是什么?ThreadLocal它内部的一个数据结构是什么?等等一系列关于ThreadLocal的问题。所以特意带来一篇关于ThreadLocal的帖子帮助你们!

正文

ThreadLocal的使用
public class TestThreadLocal {

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Utils.get());
            }).start();
        }

    }

    static class Utils{
        private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();

        static ReentrantLock get(){
            ReentrantLock reentrantLock = THREAD_LOCAL.get();
            if(reentrantLock!=null){
                return reentrantLock;
            }
            reentrantLock = new ReentrantLock();
            THREAD_LOCAL.set(reentrantLock);
            return reentrantLock;
        }
    }
}

控制台输出如下:

 可以看到我主线程开启了5个线程,但是每个线程获取到的内容都不一样。

ThreadLocal的作用

1.线程隔离:在多线程的情况下,每个线程访问的ThreadLocal变量都是互不受影响,也就是造成一个线程之间的隔离。在多线程的情况下想要对一个非并发安全的变量做一个并发处理,并且该变量不需要多个线程之间的共享。不想考虑上锁的情况下可以使用到ThreadLocal来保证每个线程都独立拥有此变量

2.线程共享变量:在同一个线程中获取到的ThreadLocal变量都是同一个。

ThreadLocal底层源码解读

提出疑问

线程之间的隔离,目的不就是为了每个线程之间获取的ThreadLocal变量不一样吧?那不简单我在Utils类中的get()方法中直接就是运行这个方法就new一个对象不就行了。现在的代码如下:

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+Utils.get());
//                System.out.println(Thread.currentThread().getName()+Utils.get());
            },"t"+i).start();
        }

    }
    static class Utils{
        private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();

//        static ReentrantLock get(){
//            ReentrantLock reentrantLock = THREAD_LOCAL.get();
//            if(reentrantLock!=null){
//                return reentrantLock;
//            }
//            reentrantLock = new ReentrantLock();
//            THREAD_LOCAL.set(reentrantLock);
//            return reentrantLock;
//        }
        static ReentrantLock get(){
            return new ReentrantLock();
        }
    }

这不一样吗?不也是并发的同时,每个线程中的ThreadLocal变量不一致吗?博主你是个**把。

此时我们忽略了他的特性就是线程之间变量共享。我把我的main方法的注释给释放再运行一把。

我们在吧Utils注释的打开再运行一把。

很显然通过ThreadLocal实现的get()方法保证了线程隔离+线程共享变量。

能否使用HashMap来替代ThreadLocal?

那我们再思考之前我改的代码中是实现了线程隔离,但是没有实现线程共享变量。那我们在之前的代码的基础下加入缓存不就可以了吗?让每个线程获取到的变量都是从缓存中获取。代码如下:

public class TestThreadLocal {

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+Utils.get());
                System.out.println(Thread.currentThread().getName()+Utils.get());
            }).start();
        }

    }

//    static class Utils{
//        private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
//
//        static ReentrantLock get(){
//            ReentrantLock reentrantLock = THREAD_LOCAL.get();
//            if(reentrantLock!=null){
//                return reentrantLock;
//            }
//            reentrantLock = new ReentrantLock();
//            THREAD_LOCAL.set(reentrantLock);
//            return reentrantLock;
//        }
        static ReentrantLock get(){
            return new ReentrantLock();
        }
//    }

    static class Utils{
        private static Map cacheMap = new HashMap<>();

        static ReentrantLock get(){
            String threadName = Thread.currentThread().getName();

            ReentrantLock reentrantLock = cacheMap.get(threadName);
            if (reentrantLock==null) {
                reentrantLock = new ReentrantLock();
                cacheMap.put(threadName,reentrantLock);
            }
            return reentrantLock;
        }
    }
}

我们使用到一个全局的HashMap来实现了一个缓存,key为线程名,value为线程共享变量。那么在并发的过程中,能否实现线程隔离和线程共享呢?

 从控制台输出结果来说并没有任何的问题,并且从多线程角度来说,并发访问Utils的get方法,但是每个线程名不一样,所以也不会造成线程安全问题?那么就真的安全吗?

我们知道HashMap它是线程不安全的一个Map集合,并且这里的HashMap还是一个全局变量,所以当我们的并发量一旦上来了肯定会有不安全的问题产生。这里肯定又有小伙伴要说用线程安全的Map集合啊,比如concurrentHashMap就可以。但是忽略了一个问题,本次的前提是无锁的情况,concurrentHashMap内部是sync同步锁+cas自旋锁来保证线程安全的,所以所以我们接着看ThreadLocal是如何保证的线程隔离+线程共享变量的。

ThreadLocal的get()方法
public T get() {

    // 获取到当前线程
    Thread t = Thread.currentThread();

    // 获取到当前线程的ThreadLocalMap对象
    // ThreadLocalMap对象是ThreadLocal的一个静态内部类。
    // ThreadLocalMap内部也是一个key,value的Entry节点数组
    // 并且Entry节点的key是当前ThreadLocal对象。
    // 并且也是通过一种Hash算法来指定新元素的位置
    // 可以看到下面代码中有getMap()方法的讲解
    ThreadLocalMap map = getMap(t);

    // 看到这里就能明白是一个懒加载机制,当没有就去创建
    if (map != null) {

        // map存在的分支
        // 因为内部的ThreadLocalMap的Entry节点的key是ThreadLocal对象
        // 所以getEntry()方法通过当前的ThreadLocal获取到Entry节点
        // 可以看到下面代码中有getEntry()方法的讲解
        ThreadLocalMap.Entry e = map.getEntry(this);

        // 这个分支是Entry节点存在的分支,存在就直接返回。
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }

    // map不存在的分支,或者是当前ThreadLocal节点不存在的分支
    // 这里达标要不就是map不存在,也不就是Entry节点不存在
    // setInitialValue内部也就是初始化ThreadLocalMap或者是添加一个
    return setInitialValue();
}



// 上面代码中getMap()方法的具体代码
// 从当前线程Thread对象中获取到Thread类维护的ThreadLocalMap对象。
// 所以每个线程中维护了一个ThreadLocalMap对象。由图可见
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}



// 上面代码中getEntry()方法的具体代码
private Entry getEntry(ThreadLocal key) {

    // 通过hash算法算出当前key的数组下标位置
    int i = key.threadLocalHashCode & (table.length - 1);
    
    // 获取到index下标的entry对象
    Entry e = table[i];

    // 这里我们要考虑,Hash算法可能还是会出现冲撞的情况
    // ThreadLocalMap对冲撞是继续往下一个节点插入,所以查找也是往下一个查找
    // 这里if是判断是否存在,或者是发生了hash冲撞,当前key的Entry的值在别的数组下标位置。这里大家好好理解一下。
    if (e != null && e.get() == key)

        // 如果没发生冲撞就直接返回
        return e;
    else
    
        // 能到else就达标是发生了hash冲撞或者是当前索引值为null。
        // getEntryAfterMiss()就不往里面追了就是再循环遍历当前索引值后面的值,找到key和当前ThreadLocal相等的就返回。
        // 这个方法在循环判断下个节点的时候,如果循环到的节点的key为null就会把他的value都置为null给gc垃圾回收。
        return getEntryAfterMiss(key, i, e);
}



// 上面代码中setInitialValue()方法的代码 
private T setInitialValue() {

    // initialValue()方法就是返回null
    T value = initialValue();

    // 获取到当前线程
    Thread t = Thread.currentThread();

    // 算是doublecheck把,不过暂时没理解double check的意义,不能通过方法直接传参吗???
    // 获取到当前线程的map对象
    ThreadLocalMap map = getMap(t);

    // 如果map不为null就代表是当前线程已经有ThreadLocalMap了,但是当前key的ThreadLocal的hash索引值的entry节点为null
    if (map != null)

        // 当前entry为null,但是目前是get()方法,所以就给索引下标的key的entry对象占个位,value为null
        map.set(this, value);
    else

        // 给当前线程创建一个ThreadLocalMap
        // 并且也会跟上面set()方法操作一样,获取到索引下标,然后站位。value为null
        createMap(t, value);
    return value;
} 

 具体说明看代码块中的注释,特别的详细,不过这里我还是给小伙伴们大概讲解一下。

就是获取到当前线程的ThreadLocalMap对象,然后如果是null,就进行一个初始化操作(懒加载)。如果ThreadLocalMap对象已经存在就直接通过hash算出下标然后取值。如果ThreadLocalMap不存在或者是当前hash算出的下标值不存在就直接创建给当前线程创建ThreadLocalMap然后站位如果map存在就直接索引位置站位。

每个线程中存在一个ThreadLocalMap对象

 这是ThreadLocal类和Thread的关系。

ThreadLocal类定义一套api对当前线程的ThreadLocalMap做一些操作,并且ThreadLocalMap是ThreadLocal的一个静态内部类。而Thread内部维护了一个ThreadLocalMap对象。所以就是每个线程都有特有的ThreadLocalMap对象。这就是每个线程的一个隔离和线程变量共享。

 

ThreadLocal的set()方法
public void set(T value) {
    
    // 获取到当前线程
    Thread t = Thread.currentThread();

    // 获取到当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);

    // 判空操作
    if (map != null)

        // 将当前ThreadLocal作为key,传进来的值为value封装成Entry对象添加到ThreadLocalMap中
        // 具体实现看下面的set()方法
        map.set(this, value);
    else

        // 如果ThreadLocalMap不存在就创建
        // 并且通过当前ThreadLocal对象hash算法获取到下标索引值,将ThreadLocal作为key,value作为value封装成Entry添加到当前线程的ThreadLocalMap中。
        createMap(t, value);
}



// 上面的set方法的具体实现
private void set(ThreadLocal key, Object value) {

    // 获取到ThreadLocalMap中维护的一个Entry[]数组。 而ThreadLocalMap属于一个线程Thread对象的
    Entry[] tab = table;

    // 获取到数组的长度,因为要通过长度来hash算法
    int len = tab.length;
    
    // hash算法算出下标索引值
    int i = key.threadLocalHashCode & (len-1);

    // 因为考虑可能存在hash的一个冲突,所以使用到一个for循环来做处理
    // 退出条件是e==null就退出
    // 所以加入没冲突就直接跳过for循环
    // 然后for循环就是从索引值开始,然后下一位、下一位的循环判断
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
    
        // 获取到当前e的key
        ThreadLocal k = e.get();

        // 如果key值是一致的,就直接替换value值
        if (k == key) {

            // 替换并直接退当前返回
            e.value = value;
            return;
        }

        // e!=null,但是k==null就代表key值被回收了但是value还未被未收
        if (k == null) {
        
            // 替换原有索引值下标的value,然后把key设置为当前ThreadLocal
            replaceStaleEntry(key, value, i);

            // 直接退出此方法,并不是退出循环哦
            return;
        }
    }

    // 当前索引位置的Entry为null,或者是循环遍历出后面的位置的Entry为null
    // 就直接创建一个Entry,放入到ThreadLocalMap中维护的Entry[]数组中
    tab[i] = new Entry(key, value);

    // 数组大小加一,方便后面扩容
    int sz = ++size;

    // 达到数组的阈值就清楚,并且会把里面需要清理的Entry给清理,方便GC回收。
    if (!cleanSomeSlots(i, sz) && sz >= threshold)

        // 扩容和清理Entry
        // 扩容大小是原有数组的2倍
        rehash();
}

具体说明看代码块中的注释。不过这里还是大概的讲解一下吧。

set()方法就是获取到当前线程对象的ThreadLocalMap,然后对其判断,如果不为null就通过ThreadLocalMap中的set()方法将当前ThreadLocal和值封装成Entry添加到Map中。如果为null就给当前线程对象创建ThreadLocalMap对象,并且也会把当前ThreadLocal对象和值封装成Entry添加到新建的ThreadLocalMap中。

如何解决内存泄漏

弱引用:只要发生了GC就会回收使用了弱引用的变量。

我们看到Entry节点继承了弱引用,在构造方法中,给key值使用了弱引用。

而我们的get()和set()方法中有多处是判断key是否为null,就替换或者是把value也置为null等待GC回收。

既然他继承了弱引用,那我们是不是就不用去管理他的一个内存了,让GC自己去回收key,然后通过代码去回收value呢?

显然是不行的?为什么不行,那这**(大可爱)开发人员为什么要这么设计?是不是**(大可爱)啊!

我们要知道GC回收之前,要通过GC Roots判定是不是垃圾,或者看是否存在强软弱虚的一个引用。而我们的ThreadLocal是一个静态变量一直对heap中的ThreadLocal做一个引用。所以是无法产生GC垃圾回收去回收他的key值,所以在set()和get()两个方法中的一些对key的一个判断都是不为null,所以没办法将value置为null,所以就存在一个永远无法回收的一个问题存在,就存在了内存泄漏问题。

此时我们看到remove()方法中。

public void remove() {

    // 获取到当前线程的ThreadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 清理操作,具体实现看下面的代码块
        m.remove(this);
}


private void remove(ThreadLocal key) {

    // 获取到ThreadLocalMap的当前的Entry[]数组
    Entry[] tab = table;

    // 获取到长度
    int len = tab.length;

    // 因为在Entry中,key是ThreadLocal对象
    // 获取到下标索引
    int i = key.threadLocalHashCode & (len-1);

    // 开始循环,因为可能存在hash冲撞
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {

        // hash再怎么冲撞,key值相等就是王道
        if (e.get() == key) {

            // key置为null
            e.clear();

            // 将value也置为null
            expungeStaleEntry(i);
            return;
        }
    }
}

可以看到remove()方法就是把当前ThreadLocal作为的key的value置为null和key置为null,等待GC回收,不过remove()需要开发者自己调用,当然开发者肯定知道哪里你不需要使用ThreadLocal变量了就可以手动调用remove()方法来防止内存泄漏问题。

 

总结

总结来说呢,难度比较小,大家可以自己追一追。

课程视频推荐(免费)

因为文字描述一些内容可能存在理解的差异,视频来理解比较好一些。所以特意给大家推荐视频课程来学习,当然肯定是免费的。

ThreadLocal视频学习地址https://www.bilibili.com/video/BV15b4y117RJ?p=95

最后如果本帖有帮助到你,希望能点赞+关注,您的支持是博主最大的动力,后续会一直更新各种源码帖。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/755171.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号