ThreadLocal原理分析

ThreadLocal简介
ThreadLocal线程本地变量的副本,对一个线程内的变量的修改不影响其它线程的变量。即在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。

①ThreadLocal实例通常在类中被定义为private static
②ThreadLocal在线程的生命周期内起作用
③空间换时间的设计思想

ThreadLocal用法
首先了解下ThreadLocal的基本用法

private static ThreadLocal<Map<String, String>> threadLocal = new MyThreadLocal();
    private static class MyThreadLocal extends ThreadLocal<Map<String, String>>{
        @Override
        protected Map<String, String> initialValue() {
            Map map = new HashMap<String, String>();
            map.put("andy","18");
            return map;
        }
    }

    public static void main(String[] args) {
        Map<String, String> result = threadLocal.get();
        System.out.println(result.get("andy"));
        result.put("lvy", "28");
        threadLocal.set(result);
        System.out.println(result.get("andy"));
        System.out.println(result.get("lvy"));
    }

ThreadLocal源码分析
接下来分析各个方法的源码

// 将此线程局部变量的当前线程副本中的值设置为指定值
public void set(T value) {
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap,有直接设置value、没有新建
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

void createMap(Thread t, T firstValue) {
    // 创建ThreadLocalMap实例,并与当前线程绑定
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 返回此线程局部变量的当前线程副本中的值
setInitialValue()
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从当前线程的ThreadLocalMap中查找Entry,如果不必为null返回value,否则设置初值并返回
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

// 移除此线程局部变量当前线程的值
public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         // 从当前线程的ThreadLocalMap中删除
         m.remove(this);
 }

private T setInitialValue() {
    // initialValue返回此线程局部变量的当前线程的初始值
    T value = initialValue(); //未覆盖就是null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

Thread与ThreadLocal的关系
Thread中有两个变量,类型都为ThreadLocal的静态内部类ThreadLocalMap,虽然被命名为Map,但和Map接口没任何关系,ThreadLocalMap底层是一个的散列表(可扩容的数组),并采用开放地址法来解决hash冲突。(解决hash冲突的方式,可以阅读数据结构中的一篇文章:https://blog.csdn.net/jiangtianjiao/article/details/88767614)
ThreadLocal.ThreadLocalMap threadLocals = null;  // 在ThreadLocal的createMap方法中被赋值
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 在Thread的init方法中被赋值


ThreadLocalMap.Entry定义

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

ThreadLocal内存泄露分析
首先引入一张示意图,能帮助我们直观的理解threadLocal,threadLocalMap,entry之间的关系

上图实线代表强引用,虚线表示弱引用
如果ThreadLocal Ref断开,ThreadLocal就没有一条链路可达,GC必然会被回收,因此key为null,这样一来,通过key永远访问不了value,但是存在了这样一条引用链:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,导致value既不能被回收又不能被访问,一致占用内存,除非线程死亡。
而在实际开发过程中我们经常使用线程池来维护线程,如当我们通过Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以极大可能导致内存泄漏。既然存在内存泄漏的问题,源码中必定存在相应的措施。

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;
    int i = key.threadLocalHashCode & (len-1); // 求数组下标 
    for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // 如果能找到相同的ThreadLocal,则覆盖entry的value
            e.value = value;
            return;
        }

        if (k == null) { // 这里k=null,e不为空,表示ThreadLocal被回收了
            replaceStaleEntry(key, value, i); // 处理脏entry
            return;
        }
     }

    tab[i] = new Entry(key, value); // 刚好下标i的entry为空,新建entry,直接插入
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象
// hash值就增加一个固定的大小0x61c88647。
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i); // expunge删去
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

cleanSomeSlots有两个参数:i表示set方法中刚新建插入的entry,n用于控制轮询次数,此处n是已经插入的size个数。如果从新插入entry开始,往后遍历过程中出现脏entry,则n更新为hash数组长度,然后进行expungeStaleEntry清理。

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清除当前脏entry
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    // 2.往后环形继续查找,直到遇到table[i]==null时结束
    // 为null时不会成为要清理的对象
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //3. 如果在向后搜索过程中再次遇到脏entry,同样将其清理掉
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //处理rehash的情况
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    //向前找到第一个脏entry
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
        if (e.get() == null)
           slotToExpunge = i;

    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            
            // 如果在向后环形查找过程中发现key相同的entry就覆盖并且和脏entry进行交换
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            // 如果在查找过程中还未发现脏entry,那么就以当前位置作为cleanSomeSlots的起点
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 搜索脏entry并进行清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        // 如果向前未搜索到脏entry,则在查找过程遇到脏entry的话,后面就以此时这个位置
        // 作为起点执行cleanSomeSlots
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 如果在查找过程中没有找到可以覆盖的entry,则将新的entry插入在脏entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        //执行cleanSomeSlots
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

调用ThreadLocal的get方法时,该方法主要是获取entry,同时遇到脏entry进行清理。

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                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)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

// 移除指定key的entry,同时进行脏entry清理

总结
1.ThreadLocal的解决hash冲突策略:开放地址法。
2.ThreadLocal的内存溢出:使用完ThreadLocal调用remove。
3.ThreadLocal的弱引用,当出现key为null(ThreadLocal被回收时),ThreadLocal的set,getEntry,remove都对key为null的entry进行了清理,使用弱引用的话在threadLocal生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。
3.复杂点主要在清理entry的方法,包括expungeStaleEntry,cleanSomeSlots,replaceStaleEntry。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值