Java中的ThreadLocal与ThreadLocalMap详解

 更新时间:2023年09月26日 10:11:46   作者:卑微小童  
这篇文章主要介绍了Java中的ThreadLocal与ThreadLocalMap详解,ThreadLocal 是一个线程局部变量,其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,需要的朋友可以参考下

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无法被访问却又占用着内存

问题:

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

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

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

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

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

到此这篇关于Java中的ThreadLocal与ThreadLocalMap详解的文章就介绍到这了,更多相关ThreadLocal与ThreadLocalMap内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论