zoukankan      html  css  js  c++  java
  • ThreadLocal

    ThreadLocal

    ThreadLocal提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量

    常用方法

    	//set方法
     	public void set(T value) {
            //获取当前线程
            Thread t = Thread.currentThread();
            //获取到ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            //使用的懒加载创建的方式
            //当前线程的ThreadLocalMap对象已经存在则将当前ThreadLocal对象和值放入Map当中
            if (map != null)
                map.set(this, value);
            else
            //创建对象并将当前ThreadLocal对象和值放入Map当中
                createMap(t, value);
        }
    
    	ThreadLocal.ThreadLocalMap threadLocals = null;
    	//获取ThreadLocalMap的方法,每个线程都维护了一个ThreaLocalMap对象
     	ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    

    ThreadLocalMap类

    ThreadLocalMap类是ThreadLocal的静态内部类,Entry继承了弱引用,下一次GC就会被回收。

    	static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                //TreadLocal对象存储的值
                Object value;
    			//k为key
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    

    ThreadLocalMap中调用的构造函数

    	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            	//entry数组的容量  默认为16
                table = new Entry[INITIAL_CAPACITY];
            	//根据hash与上数组的长度减一算出在数组的位置
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            	//将ThreadLocal存储到entry数组中
                table[i] = new Entry(firstKey, firstValue);
            	//记录数组的数据量
                size = 1;
            	//设置扩容阈值,为数组长度的三分之二  threshold = INITIAL_CAPACITY * 2 / 3;  
                setThreshold(INITIAL_CAPACITY);
            }
    

    set方法

    	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;
            	//threadLocalHashCode使用的是斐波那契散列函数
                int i = key.threadLocalHashCode & (len-1);
    			
            	//在解决hash冲突时,会依次向下遍历,指到找到插入的位置进行插入
                for (Entry e = tab[i];
                     e != null;
                     //i = nextIndex(i, len)  小于len 则返回 i + 1 大于len则返回0
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);
            	//设置新的值
                int sz = ++size;
            	//满足条件进行扩容
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突,已初始容量16为例,hash并与15位运算计算数组下标结果如下:

    hashCode 数组下标
    0x61c88647 7
    0xc3910c8e 14
    0x255992d5 5
    0x8722191c 12
    0xe8ea9f63 3
    0x4ab325aa 10
    0xac7babf1 1
    0xe443238 8
    0x700cb87f 15

    总结如下:

    1. 对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
    2. 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

    内存泄漏问题

    g1Iu2q.png

    我们知道Thread运行时,线程的的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。根据上图,基本分析如下:

    • 线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef
    • 当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。
    • Map实例化之后,也就拿到了该ThreadLocalMap的句柄,然后如果将当前ThreadLocal对象作为key,进行存取操作
    • 图中的虚线,表示key对ThreadLocal实例的引用是个弱引用

    内存泄漏分析

    根据上一节的内存模型图我们可以知道,由于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

    也就是说,如果Thread实例还在,但是ThreadLocal实例却不在了,则ThreadLocal实例作为key所关联的value无法被外部访问,却还被强引用着,因此出现了内存泄露。

    这里要额外说明一下,这里说的内存泄露,是因为对其内存模型和设计不了解,且编码时不注意导致的内存管理失联,而不是有意为之的一直强引用或者频繁申请大内存。比如如果编码时不停的人为塞一些很大的对象,而且一直持有引用最终导致OOM,不能算作ThreadLocal导致的“内存泄露”,只是代码写的不当而已!

  • 相关阅读:
    解决org.apache.jasper.JasperException: Failed to load or instantiate TagLibraryVal
    学java快2月了,对其应该有点清晰的认识了
    Linux(CentOS)挂载移动硬盘
    SEVERE: A child container failed during start
    JSTL 标签 详解
    转载自MSDN:Implementing a Membership Provider
    一个比较实用的服务器端模拟客户端Alert的代码
    简单的SQL分页法
    转载:Global.asax 文件 使用参考
    转载:缓存 Cache
  • 原文地址:https://www.cnblogs.com/huisunan/p/14738470.html
Copyright © 2011-2022 走看看