ThreadLocal源码分析

ThreadLocal源码分析

一,基本使用

Threadlocal的作用就是,为一个线程保存一个线程本地变量,该变量对该线程全局可知,其他线程无法干扰到该变量

简单使用:

    public static void main(String[] args) throws InterruptedException {
        //threadLocal简单使用,只有三个API
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        //set
        threadLocal.set(20);
        //get
        int i = threadLocal.get();
        System.out.println(i);
        //remove
        threadLocal.remove();
        System.out.println(threadLocal.get());
    }

我们使用**ThreadLocal解决线程局部变量统一定义问题,**多线程数据不能共享。(InheritableThreadLocal特例除外)不能解决并发问题。解决了:基于类级别的变量定义,每一个线程单独维护自己线程内的变量值(*存、取、删的功能*

二,get / set / remove方法

2.1 get方法

    public T get() {
        Thread t = Thread.currentThread();
        //通过当前线程找到threadlocalmap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //通过指定的key找到指定的entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //找到entry中的value
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

2.2 set方法

    public void set(T value) {
        //获取当前线程,当前进行再操作threadlocal的线程
        Thread t = Thread.currentThread();
        //获取当前线程的threadlocalmap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //map不为空则直接设置值,注意是当前this对象(threadlocal对象)
            map.set(this, value);
        else
            //空的话先构造,再设置值
            createMap(t, value);
    }

2.3 remove方法

     public void remove() {
         //获取当前线程的threadlocalmap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
		
     private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //要移除entry的位置
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //找到对应的key
                if (e.get() == key) {
                    // 将引用设置null,方便GC
                    e.clear();
                    //从该位置开始进行一次连续段清理
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

三,深入ThreadLocalMap

img

每个线程中都有一个ThreadLocalMap数据结构

ThreadLocal.ThreadLocalMap threadLocals = null;

在每个线程中的threadlocalmap其实是一个和hashmap很相似的数据结构,但是threadlocalmap只有数组构成,内部通过一个entry数组来保存键值对,键key是当前threadlocal对象,value则是我们希望保存的本地线程变量

ThreadLocalMap类是一个为ThreadLocal专门定制的实现。使用开放寻址法的散列表存储

基本属性

//默认容量是16个entry
private static final int INITIAL_CAPACITY = 16;
//threadlocalmap实际上就是一个entry类型的数组
private Entry[] table;
//阀值,超过了就需要rehash
private int threshold;
//表中entry的个数
private int size = 0;

entry

        static class Entry extends WeakReference<ThreadLocal<?>> {
         	//entry继承WeakReference,弱引用指向threadlocal实例
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocal在没有外部强引用时,发生GC时会被回收。所以后文会出现entry中的threadlocal对象被gc了,但是位置还占用着这种无效占用

构造threadlocalmap

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    		//开辟容量为16entry数组
            table = new Entry[INITIAL_CAPACITY];
    	    //计算firstKey在entry数组的索引
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    		//在索引位置实例化entry对象存放<firstKey,firstValue>
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //设置阀值为默认容量的2/3
    	    setThreshold(INITIAL_CAPACITY);
        }
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

threadlocalmap.set

        private void set(ThreadLocal<?> key, Object value) {
            //entry数组 tab
            Entry[] tab = table;
            //数组长度 len
            int len = tab.length;
            //获取key的索引
            int i = key.threadLocalHashCode & (len-1);
		   //从i位置开始遍历entry数组
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
			   //如果key相同
                if (k == key) {
                    //则直接覆盖旧的value
                    e.value = value;
                    return;
                }
			   //该threadlocal对象已经被gc,但是位置还被占着
                if (k == null) {
                    //则替换当前失效的k所在Entry节点
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
		   //如果原来entry[i]位置没有元素,则直接将新元素放那里
            tab[i] = new Entry(key, value);
            //更新entry的数量
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

	    //清除一些无效entry
        private boolean cleanSomeSlots(int i, int n) {
            //判断是否移出成功
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //此时i执行传进来的索引的下一个位置
                i = nextIndex(i, len);
                Entry e = tab[i];
                //如果该位置被占但是其引用为null(已经被gc回收了)
                if (e != null && e.get() == null) {
                    //则清除该位置
                    n = len;
                    removed = true;
                    //核心清理方法
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

	    //获取指定索引的下一个位置,如果是最后一个元素则获取第一个元素
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

set流程:

replaceStaleEntry

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            //当前失效节点
            int slotToExpunge = staleSlot;
            //从当前失效节点的前一个节点开始向前遍历
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                //查找并记录最前位置value为null的下标
                if (e.get() == null)
                    slotToExpunge = i;
            //此时slotToExpunge是最前位置value为null的下标
            //从当前失效节点的下一个节点开始向后遍历
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //如果找到key相同的entry,则直接覆盖旧的value
                if (k == key) {
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    if (slotToExpunge == staleSlot)
                         //i之前的节点里,没有value为null的情况
                        slotToExpunge = i;
                    //清除无效的位置
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
                //如果当前下标所在已经失效,并且向后扫描过程当中没有找到失效的Entry节点
                if (k == null && slotToExpunge == staleSlot)
                    //则slotToExpunge赋值为当前位置
                    slotToExpunge = i;
            }
            //如果并没有在table当中找到该key,则直接在当前位置new一个Entry取代原来的无效引用
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

清除指定位置及其之后的无效的key----连续段清理

因为entry是弱引用

Entry extends WeakReference<ThreadLocal<?>

弱引用对象在内存充足时不会被gc,但是内存不足会被gc,如果此时gc了内存还不足分配,就会报OOM

所以

  • 很多entry节点被GC了但是table数组中还占用着位置,不及时清理就会浪费位置
  • 在清理节点的同时,可以将后续非空的Entry节点重新计算下标进行排放,这样子在get的时候就能快速定位资源,加快效率。(rehash)
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            //将制定需要清理的entry的value设置为null
            tab[staleSlot].value = null;
            //gc该entry
            tab[staleSlot] = null;
            //entry数量-1
            size--;
            Entry e;
            int i;
            //遍历被删除索引位置后边的entry
            for (i = nextIndex(staleSlot, len);
                 //循环条件:该位置被占用
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //获取当前遍历entry当中的key
                if (k == null) {
                    //如果ThreadLocal为null,则将value以及数组下标所在位置设置null,方便GC,并且entry数量-1
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //如果不为null,则重新计算位置后放于新位置
                    int h = k.threadLocalHashCode & (len - 1);
                    //如果是当前位置则遍历下一个位置
                    if (h != i) {
                        //不是当前位置,则重新从i开始找到下一个为null的坐标进行赋值
                        tab[i] = null;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        //放到新位置
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

全表清理expungeStaleEntries

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        // 全表遍历,找到一个无效key,则做一次连续段清理
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

四,执行流程总结

img

五,内存泄漏问题

在java中,我对内存泄漏问题的理解就是,一个对象你永远不用了,但是这个对象却无法被gc

ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。(比如线程池)

如何避免?

既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("kevin");
    // 其它业务逻辑
} finally {
    //使用完threadlocal变量后显示remove
    localName.remove();
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocal源码是Java中一个关键的类,它提供了一种在多线程环境下实现线程本地变量的机制。在JDK 8之前和之后,ThreadLocal的内部结构有所变化。ThreadLocal源码分为两部分:ThreadLocal类和ThreadLocalMap类。 ThreadLocal类是一个泛型类,它包含了两个核心方法:set()和get()。set()方法用于将一个值与当前线程关联起来,get()方法用于获取当前线程关联的值。 ThreadLocalMap类是ThreadLocal的内部类,它用于存储每个线程的本地变量。在JDK 8之前,ThreadLocalMap是通过线性探测法解决哈希冲突的,每个ThreadLocal对象都对应一个Entry对象,Entry对象包含了ThreadLocal对象和与之关联的值[2]。 在JDK 8之后,ThreadLocalMap的实现方式发生了改变。使用了类似于HashMap的方式,采用了分段锁的机制来提高并发性能。每个线程维护一个ThreadLocalMap对象,其中的Entry对象也是采用链表的形式来解决哈希冲突。 总结起来,ThreadLocal源码主要由ThreadLocal类和ThreadLocalMap类组成。ThreadLocal类提供了set()和get()方法来管理线程本地变量,而ThreadLocalMap类则负责存储每个线程的本地变量,并解决哈希冲突的问题。 史上最全ThreadLocal 详解 ThreadLocal源码分析_02 内核(ThreadLocalMap) 【JDK源码】线程系列之ThreadLocal 深挖ThreadLocal ThreadLocal原理及内存泄露预防 ThreadLocal原理详解——终于弄明白了ThreadLocal ThreadLocal使用与原理 史上最全ThreadLocal 详解。 ThreadLocal源码分析,主要有ThreadLocal源码以及ThreadLocal的内部结构在jdk8前后的变化。 使用方式非常简单,核心就两个方法set/get public class TestThreadLocal { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { try { threadLocal.set("aaa"); Thread.sleep(500); System.out.println("threadA:" threadLocal.get()); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { threadLocal.set("bbb"); System.out.println("threadB:" threadLocal.get()); } }).start(); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值