前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TheadLocal底层原理超级图解,逐行讲解!!!(从0到1)

TheadLocal底层原理超级图解,逐行讲解!!!(从0到1)

原创
作者头像
天下之猴
发布2024-09-25 10:22:33
1040
发布2024-09-25 10:22:33
举报
文章被收录于专栏:笔/面试系列

先看使用用例, 下面使用ThreaLocal实现一个线程独立计数器的代码

代码语言:java
复制
public class ThreadLocalExample {  

    // 创建一个ThreadLocal变量,用于存储每个线程的计数器, 初始值设为0
    private static final ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);  

    public static void main(String[] args) {  
        // 创建多个线程并启动  
        for (int i = 0; i < 5; i++) {  
            new Thread(new CounterTask(i)).start();  
        }  
    }  

    // 内部静态类实现Runnable接口  
    private static class CounterTask implements Runnable {
        private final int threadId;  
        
        //对线程名进行赋值
        public CounterTask(int threadId) {  
            this.threadId = threadId;  
        }  

        @Override  
        public void run() {  
            // 每个线程增加自己的计数器值  
            for (int i = 0; i < 10; i++) {  
                int currentCount = threadLocalCounter.get();  
                threadLocalCounter.set(currentCount + 1);  
                System.out.println("Thread #" + threadId + " - Count: " + threadLocalCounter.get());  
            }  
        }  
    }  
}

想弄懂ThreadLocal类, 首先我们先看Thread类

Thread中对ThreadLocalMap的引用
Thread中对ThreadLocalMap的引用

可以看到Thread类中有声明ThreadLocalMap类, 而ThreadLocalMap类又是ThreadLocal的静态内部类

ThreadLocal中的静态内部类
ThreadLocal中的静态内部类

根据map我们不难想到ThreadLocalMap在ThreadLocal中是用来存储数据的,而事实也的确如此,看set和get方法可以确认, ThreadLocal的get(), set()方法实际上是通过ThreadLocalMap来对Entry对象操作来实现的, 什么是Entry对象? 大家熟悉的Map对象是键值对的集合, Entry对象就是单个的键值对

ThreadLocal的get方法
ThreadLocal的get方法
ThreadLocal的set方法
ThreadLocal的set方法

上面讲完ThreadLocal的get()和set方法, 我们来进一步分析是怎么实现的, 先来看get方法

代码语言:java
复制
 public T get() {
        //currentThread为本地方法, 通过其他语言实现, 作用为获取当前线程对象
        Thread t = Thread.currentThread();
        //返回之前在Threa类中声明的ThreadLocalMap类, 引用名为TthreadLocals, 可以看本文第二张图的184行
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        /*这种写法想必大家很熟悉, 同样Entry为ThreaLocalMap的静态内部类,这个类下面再做详解,
            只需要知道表示单个键值就行了这里 */
            ThreadLocalMap.Entry e = map.getEntry(this);
            
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取值, 并返回
                T result = (T)e.value;
                return result;
            }
        }
        //如果没有设置值的话,默认值为设为null
        return setInitialValue();
    }

这里我们再来讲下Entry怎么工作的,Map底层的实现也依赖于Entry,可以帮你们进一步了解Map,话不多上贴源码

代码语言:java
复制
static class ThreadLocalMap {
///被忽略的代码
     
 private Entry[] table;
 
 //Entry静态内部类
         static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        
        
 
  
    private Entry getEntry(ThreadLocal<?> key) {
           // 算出位置i, 根据线程对象和当前键值对的长度来计算的
            int i = key.threadLocalHashCode & (table.length - 1);
            //获取到key对应位置的单个键值对
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
            //从i开始遍历后面的元素,找到对应键的元素, key相等就返回对应元素, 为null则进行探测式清理
                return getEntryAfterMiss(key, i, e);
        }
 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;
            //获取这个键在hash中对应的位置
            int i = key.threadLocalHashCode & (len-1);
            
            //从i开始找,直到找到对应的位置
            for (Entry e = tab[i];
                //e!null表示发生了哈希冲突
                 e != null;
                 //从ThreadLocal的对象散列值开始每次往后移一位
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                
                //如何要放的位置上的键和这个键相等说明是替换,进行值更新
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果碰到过期的key, 优先使用过期的key,节省空间
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //如果到这里说明key对应的hash位置上没有数据,且Entry中的key都在被时殷弘,
            tab[i] = new Entry(key, value);
            int sz = ++size;
             // 做一次启发式清理:
            // 条件一:!cleanSomeSlots(i, sz) 成立,说明启发式清理工作未清理到任何数据..
            // 条件二:sz >= threshold 成立,说明当前table内的entry已经达到扩容阈值了..会触发rehash操作。
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
        
//被忽略的代码        
}

大致数据结构为一个 Thread 类可以有多个 ThreadLocal 类,一个 Thread 类拥有一个 ThreadLocalMap,一个ThreadLocalMap又存储了多个ThreadLocal, ThreadLocalMap的底层数据结构是数组;键值对Entry(ThreadLocal<?> k, Object v)(多个 ThreadLocal)都存储在 table 数组中,就像Map [{id:1},{name:"张三"},{age:20}]。

另外要注意的是Entry是继承了弱引用的, 而键确是强引用, 弱引用在gc时,不管内存空间够不够都会被回收, 而value不会被清理, 假如我们不做任何措施的话, vlue永远无法被回收, 从而导致内存泄漏

不过ThreadLocal也有应对这种情况的策略, 上面代码中的replaceStaleEntry方法就是使用新增的key代替原来的key,和value, 并且set(), get(). remove()等方法时都会清理key为null的记录

总结:

  1. ThreadLocal是线程的专属本地变量,可以有多个的, 其中键是线程对象进行斐波那契散列得到的值, 如果发生了哈希冲突, 其解决方案是开放地址法,而不是拉链寻址法, 即继续往后找空位置, 而不是往下找空位置
  2. Entry在ThreadLocalMap中是继承了弱引用的, 其键是弱引用, 每次gc时没有都会被清理,value时强引用, 线程不被回收就不会被回收, 如果使用的是线程池就有可能导致value的内存泄漏,所以最好使用remove()方法把整个entry去掉
  3. ThreadLocalMap作为属性在Thread中的,一个线程一个ThreadLocalMap, 一个ThreadLocalMap多个ThreadLocal

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档