ThreadLocal源码分析

[toc]html

ThreadLocal源码分析

简单分析

什么是ThreadLocal

ThreadLocal顾名思义能够理解为线程本地变量。也就是说若是定义了一个ThreadLocal,每一个线程往这个ThreadLocal中读写是线程隔离(也就是每一个线程读写都是本身的一份独立对象,与其余线程是无关的,固然前提是不一样线程set的不是同一个对象的引用),互相之间不会影响的。它提供了一种将可变数据经过每一个线程有本身的独立副本从而实现线程封闭的机制。java

大体实现思路

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每一个线程有一个本身的ThreadLocalMap。ThreadLocalMap有本身的独立实现,能够简单地将它的key视做为ThreadLocal,value为代码中放入的值(实际上key并非ThreadLocal自己,而是他的一我的弱引用)。每一个线程在往某个ThreadLocal里赛值得时候,都会往本身的ThreadLocalMap里存,读也是以某个ThreadLocal做为引用,在本身的map里找对应的key,从而实现了线程隔离。web

使用场景

1.如在一个AccountService中写了一段相似这样的代码:编程

Context ctx = new Context();
ctx.setTrackerID(.....)

而后这个AccountService 调用了其余Java类,不知道通过了多少层调用之后,最终来到了一个叫作AccountUtil的地方,在这个类中须要使用Context中的trackerID来作点儿事情: 在这里插入图片描述 很明显,这个AccountUtil没有办法拿到Context对象,怎么办? 1)能够在调用每一个方法的时候,讲Context对象一层一层往下传递。 存在的问题:会修改不少类的代码,更重要的是有些类是第三方的根本没有可能去修改源代码。 2)讲Context中的set/get方法改为静态的,而后再AccountUtil中直接Context.get调用便可api

public class Context{
    public static String getTrackerID(){
        ......
    }
    public static void setTrackerID(String id){
        ......
    }
}

这的确解决了一层一层传递的麻烦,可是出现了一个致命的问题。多线程并发问题!!!!!!!!!! 3)这个时候咱们能不能把这个值放到线程中?让线程携带这个值,这样咱们不管在任何地方均可以轻松获取,且是线程安全的。也就是每一个线程都有一个私家领地。数组

public class Context {
    private static final ThreadLocal<String> mThreadLocal 
        = new ThreadLocal<String>();

    public static void setTrackerID(String id) {
        mThreadLocal.set(id); 
    }   
    public static String getTrackerID() {
        return mThreadLocal.get();
    }   
}

2.以session为列理解ThreadLocal 在web开发的session中,不一样的线程对应不一样的session,那么如何针对不一样的线程获取对应的session呢? 1)在action中建立session,而后传递给Service,Service再传递给Dao,很明显,这种方式将代码变得臃肿复杂。 2)使用一个类,SesssionFacoty里面封装getSeesion的静态方法,而后在每一个Dao中SesssionFacoty.getSession()。存在并发访问。 固然,对其方法加锁,这样的话效率上存在必定的问题。 3)建立一个静态的map,键对应咱们的线程,值对应session,当咱们想获取session时,只须要获取map,而后根据当前的线程就能够获取对应的值。tomcat

private static final ThreadLocal<Session> threadSession = new ThreadLocal();
public static Session getSession() throws InterruptedException {
    Session s = threadSession.get();
    try {
        if (null == s){
            s = getSessionFactory.openSession();
            threadSession.set(s);
        }
    }catch (HibernateException e){
        throw  new InfrastructureExecption(e);
    }
    return s;
}

在Hibernate中经过使用ThreadLocal来实现的。在getSession方法中,若是ThreadLocal存在session,则返回session,不然建立一个session放入ThreadLocal中。安全

源码分析

ThreadLocalMap

存储结构

/**
 * The entries in this hash map extend WeakReference, using
 * its main ref field as the key (which is always a
 * ThreadLocal object).  Note that null keys (i.e. entry.get()
 * == null) mean that the key is no longer referenced, so the
 * entry can be expunged from table.  Such entries are referred to
 * as "stale entries" in the code that follows.
 */
 //自带一种基于弱引用的垃圾清理机制
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //key为ThreadLocal
    Entry(ThreadLocal<?> k, Object v) {
    	//ThreadLocal中存放的是ThreadLocal的弱引用
    	//这里虽然使用弱引用在key能够被回收,可是value还存在,因此不正确使用ThreadLocal可能会出现内存溢出
        super(k);
        value = v;
    }
}

为何须要使用弱引用?

由于若是这里使用普通的key-value形式来定义存储结果,实质上就会形成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,那么GC就没办回收,而程序自己也没法判断是否能够清理节点。 弱引用是Java中四档引用中的第三档,比软引用更加弱一些,若是一个对象没有强引用链可达,那么通常活不过下次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap自己的垃圾清理提供了便利。session

引用类型

1)强引用:就是指程序代码中广泛存在的,相似于“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象,也就是说即便Java虚拟机内存空间不足时,GC收集器也毫不会回收该对象,若是内存空间不够就会致使内存溢出。 2)软引用:用来描述一些还有用,但并不是必需的对象。对于软引用关联着的对象,在系统要发生内存溢出异常以前,将会把这些对象列进回收范围之中并进行回收,以避免出现内存溢出。若是此次回收仍是没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 以后,提供了 SoftReference 类来实现软引用。 2)弱引用:也就是用来描述非必需对象的,可是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生以前。当垃圾回收器工做时,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK 1.2 以后,提供了 WeakReference 类来实现弱引用。ThreadLocal使用到的就有弱引用。 4)虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例。为一个对象设置虚引用关联的惟一目的就是但愿能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2 以后,提供了PhantomReference 类来实现虚引用。 在这里插入图片描述多线程

ThreadLocal内存溢出

ThreadLocalMap使用ThreadLocal的弱引用做为key,若是一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,若是当前线程再迟迟不结束的话,这些key为null的Entry的value会一直存在一条强引用链:Thread Ref → Thread → ThreaLocalMap → Entry → value永远没法回收,形成内存泄漏。 其实,ThreadLocalMap的设计中已经考虑到这种状况,也加上了一些防御措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里全部key为null的value。

可是这些被动的预防措施并不能保证不会内存泄漏。 1)使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能致使的内存泄漏 2)分配使用了ThreadLocal又再也不调用get(),set(),remove()方法,那么就有可能会致使内存泄漏。

其实ThreadLocal是否会引发内存泄漏也是一个比较有争议性的问题。 认为ThreadLocal会引发内存泄漏的说法是由于若是一个ThreadLocal对象被回收了,咱们往里面放的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,所以value不会被回收。 认为ThreadLocal不会引发内存泄漏的说法是由于ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。 之因此有关于内存泄露的讨论是由于在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。若是线程不会复用,用完即销毁了也不会有ThreadLocal引起内存泄露的问题。《Effective Java》一书中的第6条对这种内存泄露称为unintentional object retention(无心识的对象保留)。 当咱们仔细读过ThreadLocalMap的源码,咱们能够推断,若是在使用的ThreadLocal的过程当中,显式地进行remove是个很好的编码习惯,这样是不会引发内存泄漏。 那么若是没有显式地进行remove呢?只能说若是对应线程以后调用ThreadLocal的get和set方法都有很高的几率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。 但不管如何,咱们应该考虑到什么时候调用ThreadLocal的remove方法。一个比较熟悉的场景就是对于一个请求一个线程的server如tomcat,在代码中对web api做一个切面,存放一些如用户名等用户信息,在链接点方法结束后,再显式调用remove。

类成员变量与相应方法

/**
 * The initial capacity -- MUST be a power of two.
 * 初始容量,必须为2的幂
 */
 private static final int INITIAL_CAPACITY = 16;
 /**
  * Entry表,大小为2的幂
  * 对于2的幂做为模数取模,能够用&(2^n-1)来替代%2^n,位运算比取模效率高不少。
  * 至于为何,由于对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。
  */
 private Entry[] table;
 /**
  * 表里entry的个数
  */
 private int size = 0;
 /**
  * 从新分配表大小的阙值,默认为0
  */
 private int threshold; // Default to 0
 //色湖之resize阙值以维持最坏的2/3的装载因子
 private void setThreshold(int len) {
   threshold = len * 2 / 3;
}
/**
* 使得Entry造成一个环形。因此它这里并非像HashMap中,是一个数组,而后每一个数组中下面挂上一个链路(java8是二叉树),
* 在这里它使用的是线性探测法,即若是hash索引到某个index,产生冲突会往下遍历寻找下一个。
*/
private static int nextIndex(int i, int len) {
  return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* 构造一个包含firstKey和firstValue的map
* ThreadLocalMap是惰性构造的,因此只有当至少要往里面放一个元素的时候才会构建它。
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    //
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //色号顶扩容阙值
    setThreshold(INITIAL_CAPACITY);
}

getEntry方法

这个方法会被ThreadLocal的get方法直接调用,用于获取map中某个ThreadLocal存放的值。

private Entry getEntry(ThreadLocal<?> key) {
    // 根据key这个ThreadLocal的ID来获取索引,也即哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回
    if (e != null && e.get() == key) {
        return e;
    } else {
        // 由于用的是线性探测,因此日后找仍是有可能可以找到目标Entry的。
        return getEntryAfterMiss(key, i, e);
    }
}
/*
 * 调用getEntry未直接命中的时候调用此方法
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 基于线性探测法(一个一个往下找)不断向后探测直到遇到空entry。
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到目标
        if (k == key) {
            return e;
        }
        if (k == null) {
            // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
            expungeStaleEntry(i);
        } else {
            // 环形意义下日后面走,
            i = nextIndex(i, len);
        }
        e = tab[i];
    }
    return null;
}
/**
 * 这个函数是ThreadLocal中核心清理函数,它作的事情很简单:
 * 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
 * 另外,在过程当中还会对非空的entry做rehash。
 * 能够说这个函数的做用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // 由于entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
    tab[staleSlot].value = null;
    // 显式设置该entry为null,以便垃圾回收
    tab[staleSlot] = null;
    size--;
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 清理对应ThreadLocal已经被回收的entry
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            /*
             * 对于尚未被回收的状况,须要作一次rehash。
             * 若是对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
             * 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
             */
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null; 
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    // 返回staleSlot以后第一个空的slot索引
    return i;
}

总结: 1)根据入参threadLocal的threadLocalHashCode对表容量取模获得index 2)若是index对应的solt就是要读的threadLoacl,则直接返回结果 3)调用getEntryAfterMiss线性探测,过程当中每碰到无效slot,调用expungeStaleEntry进行段清理(清理只是再get没有获得的状况下才有可能发生);若是找到了key,则返回结果entry 4)没有找到key,返回null

set方法

private void set(ThreadLocal<?> key, Object value) {
    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();
        // 找到对应的entry
        if (k == key) {
            e.value = value;
            return;
        }
        // 替换失效的entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // 向前扫描,查找最前的一个无效slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len)) {
        if (e.get() == null) {
            slotToExpunge = i;
        }
    }
    // 向后遍历table
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 找到了key,将其与无效的slot交换
        if (k == key) {
            // 更新对应slot的value值
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            /*
             * 若是在整个扫描过程当中(包括函数一开始的向前扫描与i以前的向后扫描)
             * 找到了以前的无效slot则以那个位置做为清理的起点,
             * 不然则以当前的i做为清理起点
             */
            if (slotToExpunge == staleSlot) {
                slotToExpunge = i;
            }
            // 从slotToExpunge开始作一次连续段的清理,再作一次启发式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 若是当前的slot已经无效,而且向前扫描过程当中没有无效slot,则更新slotToExpunge为当前位置
        if (k == null && slotToExpunge == staleSlot) {
            slotToExpunge = i;
        }
    }

    // 若是key在table中不存在,则在原地放一个便可
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 在探测过程当中若是发现任何无效slot,则作一次清理(连续段清理+启发式清理)
    if (slotToExpunge != staleSlot) {
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
}

/**
 * 启发式地清理slot,
 * i对应entry是非无效(指向的ThreadLocal没被回收,或者entry自己为空)
 * n是用于控制控制扫描次数的
 * 正常状况下若是log n次扫描没有发现无效slot,函数就结束了
 * 可是若是发现了无效的slot,将n置为table的长度len,作一次连续段的清理
 * 再从下一个空的slot开始继续扫描
 * 
 * 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,
 * 区别是前者传入的n为元素个数,后者为table的容量
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // i在任何状况下本身都不会是一个无效slot,因此从下一个开始判断
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 扩大扫描控制因子
            n = len;
            removed = true;
            // 清理一个连续段
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

private void rehash() {
    // 作一次全量清理
    expungeStaleEntries();
    /*
     * 由于作了一次清理,因此size极可能会变小。
     * ThreadLocalMap这里的实现是调低阈值来判断是否须要扩容,
     * threshold默认为len*2/3,因此这里的threshold - threshold / 4至关于len/2
     */
    if (size >= threshold - threshold / 4) {
        resize();
    }
}

/*
 * 作一次全量清理
 */
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null) {
            /*
             * 我的以为这里能够取返回值,若是大于j的话取了用,这样也是可行的。
             * 由于expungeStaleEntry执行过程当中是把连续段内全部无效slot都清理了一遍了。
             */
            expungeStaleEntry(j);
        }
    }
}

/**
 * 扩容,由于须要保证table的容量len为2的幂,因此扩容即扩大2倍
 */
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; 
            } else {
                // 线性探测来存放Entry
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null) {
                    h = nextIndex(h, newLen);
                }
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
}

总结: 1)探测过程当中slot都不无效,而且顺利找到key所在的slot,直接替换便可 2)在探测过程当中发现无效solt,调用replaceStableEntry,效果是最终必定会把key和value放在这个slot,而且会尽量清理无效slot

  • 在replaceStaleEntry过程当中,若是找到了key,则作一个swap把它放到那个无效slot中,value置为新值
  • 在replaceStaleEntry过程当中,没有找到key,直接在无效slot原地放entry 3)探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,作一次启发式清理,若是没清理出去key,而且当前table大小已经超过阈值了,则作一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,若是完了以后table大小超过了threshold - threshold / 4,则进行扩容2倍

remove方法

/**
 * 从map中删除ThreadLocal
 */
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;
        }
    }
}

ThreadLocal

Thread、ThreadLocal,ThreadLocalMap关系

在这里插入图片描述 一个Thread中只有一个ThreadLocalMap(Thread中有一个成员变量ThreadLocal.ThreadLocalMap threadLocals = null;),一个ThreadLocalMap中能够有多个ThreadLoacl对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中一个的Entry实体。

ThreadLoacl源码分析

public void set(T value) {
   //获取调用改set方法的线程
   Thread t = Thread.currentThread();
   //根据线程去获取ThreadLocalMap,若是还为初始化,那么就调用createMap建立初始化,构造直接map.set(this,value)便可
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	//直接调用ThreadLocalMap的set方法
        map.set(this, value);
    else
    	//线程第一次set,初始化ThreadLocalMap
        createMap(t, value);
}
//初始化Tread中的ThreadLocal.ThreadLocalMap threadLocals
void createMap(Thread t, T firstValue) {
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}

public T get() {
    Thread t = Thread.currentThread();
    //获取该线程的ThreadLoalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //若是ThreadLocalMap还未被初始化,就返回默认值
    return setInitialValue();
}

//Removes the current thread's value for this thread-local variable. 
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

参考博客

一个故事讲明白ThreadLocal:https://mp.weixin.qq.com/s/aM03vvSpDpvwOdaJ8u3Zgw ThreadLocal源码解读:http://www.javashuo.com/article/p-mksbvkvj-dm.html Java多线程编程-(18)-借ThreadLocal出现OOM内存溢出问题再谈弱引用WeakReference:https://blog.csdn.net/xlgen157387/article/details/78513735?ref=myread

相关文章
相关标签/搜索