ThreadLocal与ThreadLocalMap

ThreadLocal与ThreadLocalMap【jdk 1.8】

使用场景

  • 每个线程需要一个独享的对象(通常是工具类)
  • 每个线程内需要保存全局变量,可以在不同的地方直接获取,避免参数传递的麻烦

作用

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己独享的对象)

  • 任何方法中都可以轻松获取其对象

    好处

    • 可以达到线程安全
    • 不需要加锁,提高效率
    • 高效利用内存,相比于每个任务都新建一个对象,用ThreadLocal可以节省内存和开销
    • 免去传递参数的繁琐,降低了程序耦合度

主要方法

1)initialValue()

该方法会返回当前线程对应的初始值,采用了懒加载机制,当第一次get的时候才会触发,当线程第一次使用get方法的时候才会触发。除非线程先前调用了set方法,在这种情况下,不会再调用InitValue方法

2)set(T value)

未当前线程设置一个新的值

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}
T get()
public T get() {
    Thread t = Thread.currentThread();//获取当前线程
    ThreadLocalMap map = getMap(t);//从当前线程中获取ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//获取Entry
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;//返回对象
        }
    }
    return setInitialValue();//如果第一次调用get,ThreadLocalMap未空或者在ThreadLocalMap中还未存储对象,则进行初始化并返回存储对象
  }
remove()
//移除线程所存储对象
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

原理

Thread类中又这样一个ThreadLocalMap 类型成员变量threadLocals

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 是ThreadLocal的内部类,其结构如HashMap很相似,在其内部还有个Entry,保存ThreadLocal和其保存的对象。其默认容量也为16,负载因子未2/3,并且不存在next指针,哈希冲突后采用的延后策略。具体请看最后问题栏

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {

        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private static final int INITIAL_CAPACITY = 16;

    private Entry[] table;
    private int size = 0;

    private int threshold; //阈值
    
    private void setThreshold(int len) {
            threshold = len * 2 / 3;  //负载因子是2/3,
    }
    //......省略............
}

在这里插入图片描述

总的来说,在Thead中维护了一个Map,在Map中存储了ThreadLocal和其绑定的对象

每次获取对象都会从当前线程中获取map并将ThreadLocal传入从而获得对象

到底会不会引起内存泄露

ThreadLocal被用作TheadLocalMap的弱引用key,这种设计也是ThreadLocal被讨论内存泄露的热点问题,因此有必要了解一下什么是弱引用。

弱引用

弱引用是用来描述非必须的对象的,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次GC发生之前,也就是说下一次GC就会被回收。JDK1.2之后,提供了WeakReference来实现弱引用。

​ 由于ThreadLocalMap是以弱引用的方式引用着ThreadLocal,换句话说,就是ThreadLocal是被ThreadLocalMap以弱引用的方式关联着,因此如果ThreadLocal没有被ThreadLocalMap以外的对象引用,则在下一次GC的时候,ThreadLocal实例就会被回收,那么此时ThreadLocalMap里的一组KV的K就是null了,因此在没有额外操作的情况下,此处的V便不会被外部访问到,而且只要Thread实例一直存在,Thread实例就强引用着ThreadLocalMap,因此ThreadLocalMap就不会被回收,那么这里K为null的V就一直占用着内存

综上,发生内存泄露的条件是

  • ThreadLocal实例没有被外部强引用,比如我们假设在提交到线程池的task中实例化的ThreadLocal对象,当task结束时,ThreadLocal的强引用也就结束了
  • ThreadLocal实例被回收,但是在ThreadLocalMap中的V没有被任何清理机制有效清理
  • 当前Thread实例一直存在,则会一直强引用着ThreadLocalMap,也就是说ThreadLocalMap也不会被GC
示例
class Test{

    byte data[]=new byte[1024*1024*10];

    @Override
    protected void finalize() throws Throwable {
        System.out.println("destroy");
    }
}
public class ThreadLocalDemo {

    public ThreadLocal<Test> t = new ThreadLocal<>();

    public static void main(String[] args) {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        Test test = new Test();
        threadLocalDemo.t.set(test);
        test = null;


        //threadLocalDemo.t.remove();
        threadLocalDemo = null;
        System.out.println("start gc");
        System.gc();
        try {
            Thread.sleep(1000L);
        }catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("end");
    }
}
//输出
/*
start gc
end
*/

//当threadLocalDemo.t.remove();不被注释
/*
输出:
start gc
destroy
end
*/

​ 当不在持有ThreadLocalDemo对象,因为thread中ThreadLoaclMap中保存有ThreadLocal的引用 ,如果ThreadLocal不是弱引用的话,ThreadLocal是不可能被gc的。而如果ThreadLocal与ThreadLocalMap之间是弱引用,如果除Thread外没有任何对象可以获得ThreadLocal,则ThreadLocal是可以为回收的

​ 当然,其仍然仍然存在一定的内存泄露,即value与TreadLcoalMap之间存在引用,当ThreadLocal被gc时value是无法被gc的,但是在ThreadLocalMap内部也存在一些机制,当map扩容或者发生hash冲突的时候会判断key键是否为null(即判断ThreadLocal对象是否被回收),如果是null,则会将value值同样设为Null.从而帮助value gc

ThreadLocal为什么经常设置为static
public class ThreadLocalDemo2 {
    public ThreadLocal<Test> t = new ThreadLocal<>();
	//public static ThreadLocal<Test> t = new ThreadLocal<>();
    public static void main(String[] args) {
        ThreadLocalDemo2 threadLocalDemo = new ThreadLocalDemo2();
        Test test = new Test();
        test.name = "xxxx";
        threadLocalDemo.t.set(test);

        ThreadLocalDemo2 threadLocalDemo2 = new ThreadLocalDemo2();
        Test test2 = new Test();
        test2.name = "yyyyy";
        threadLocalDemo2.t.set(test2);

        System.out.println(threadLocalDemo.t.get().name);
        System.out.println(threadLocalDemo2.t.get().name);
    }
}
/*
输出:
xxxx
yyyyy

static 修饰 ThreadLocal
输出:
yyyyy
yyyyy
*/

static修饰ThreadLocal后,单个线程无论创建多个对象,其ThreadLocal示例仅仅只有一个。

如果变量ThreadLocal是非static的就会造成每次生成实例都要生成不同的ThreadLocal对象,虽然这样程序不会有什么异常,但是会浪费内存资源,甚至会造成内存泄漏.。

建议

通过前面几节的分析,我们基本弄清楚了ThreadLocal相关设计和内存模型,对于是否会发生内存泄露做了分析,下面总结下几点建议:

  • 当需要存储线程私有变量的时候,可以考虑使用ThreadLocal来实现
  • 当需要实现线程安全的变量时,可以考虑使用ThreadLocal来实现
  • 当需要减少线程资源竞争的时候,可以考虑使用ThreadLocal来实现
  • 注意Thread实例和ThreadLocal实例的生存周期,因为他们直接关联着存储数据的生命周期
    • 如果频繁的在线程中new ThreadLocal对象,在使用结束时,最好调用ThreadLocal.remove来释放其value的引用,避免在ThreadLocal被回收时value无法被访问却又占用着内存

参考:https://www.jianshu.com/p/1a5d288bdaee

问题:

  1. 为什么ThreadLocalMap不用HashMap而是自己写了个Map

    • 自定义Map限定了键值未ThreadLocal类型
    • 其Entry对象继承了弱引用类,用来存储键值,从而不影响对象被回收,而HashMap中Key是强引用
    • ThreadLocalMap在写数据和查数据的过程中有一个清理过期数据的功能,能够将发现的过期数据清理到,从某种意义上也是解决了内存泄漏问题。当然不是完全解决
  2. ThreadLocalMap达到扩容的阈值时会真正的扩容吗?

    不会,达到阈值之后,进行一个散列表的扫描清楚过期的数据,如果清理完之后,数据量仍然达到其阈值的75%,才进行扩容

    扩容源码:

    private void rehash() {
        expungeStaleEntries();//清理
    
        if (size >= threshold - threshold / 4)//数据量仍然达到其阈值的75%,才进行扩容
            resize();
    }
    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];//新建一个数组
        int count = 0;
    
        for (int j = 0; j < oldLen; ++j) {//遍历
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; //将value设为null从而帮助GC
                } else {
                    //重新进行hash
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);//采用的时自定义hash算法
                    newTab[h] = e;
                    count++;
                }
            }
        }
    
        setThreshold(newLen);//计算新的阈值
        size = count;
        table = newTab;
    }
    
  3. ThreadLocalMap获取Entry的流程

    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);//hash运算计算出位置
        Entry e = table[i];
        if (e != null && e.get() == key)//未发生过Hash冲突
            return e;
        else//发生过冲突
            return getEntryAfterMiss(key, i, e);//进行下一个位置的判断
    }
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
    
        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if (k == null)//如果为空,则说明此位置被GC了,为过期数据
                expungeStaleEntry(i);//为了防止内存泄漏,触发一个“探测式”过期数据回收逻辑
            else
                i = nextIndex(i, len);//计算下一个位置
            e = tab[i];
        }
        return null;
    }
    //“探测式”过期数据回收逻辑
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
    
        tab[staleSlot].value = null;//将value设为空,帮助GC
        tab[staleSlot] = null;
        size--;
    
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);//根据hash和寻址算法遍历所有与当前hash相同的槽点
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {//帮助GC
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                //如果key不为空,则重新进行hash,将其移动到一个更靠近其hash位置的槽点(提高下次get的效率)
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;
    
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }
    
  4. ThreadLocalMap中set的具体流程

    private void set(ThreadLocal<?> key, Object value) {
    	//寻址
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
    	//遍历可能的slot
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
    		//如果key相同,则替换
            if (k == key) {
                e.value = value;
                return;
            }
    		//如果k为空,则进行取代算法
            if (k == null) {
                //大体就是遍历可能的槽点,直到碰到key值相同的,则将其移动到距离真实hash位置最近的点,如果没有,则再最有好的位置new一个新的Entry
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        tab[i] = new Entry(key, value);
        int sz = ++size;//判断是否达到扩容条件
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值