JavaSE多线程-ThreadLocal原理(源码分析)

关于Thread类:

Thread类中维护了ThreadLocal.ThreadLocalMap属性,这就是每个线程的存储空间。相当于Map,key为当前线程,value为entry。

Public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

关于ThreadLocal:

ThreadLocal提供一个线程局部变量(thread local variable),访问到某个变量的每个线程都有自己的局部变量。提供线程保持对象的方法和避免参数传递的方便的对象访问方式

Thread是一个空间换时间的方案,为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,耗费了内存但减少线程同步所带来的线程消耗

每个线程可访问自己内部的副本变量。这样做的好处是可以保证共享变量在多线程环境下访问的线程安全性

 

ThreadLocal不是用来解决对象共享访问问题的

线程安全:多线程环境下,用ThreadLocal去保证成员变量的安全

与其他所有的同步机制相同,ThreadLocal同样是为了解决多线程中的对同一变量的访问冲突

 

ThreadLocal源码分析:(基于JDK1.8)

构造方法:

public ThreadLocal() {
}

内部方法:(非公有)

getMap():

// 返回线程类维护的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;
}

createMap()&& createInheritedMap():

给Thread类中的ThreadLocalMap对象进行赋值。将该类对象(ThreadLocal)作为key,
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

initialValue()

如果get方法没有get到数据时的默认值。可子类覆写
protected T initialValue() {
    return null;
}

setInitialValue()

get方法返回的默认策略
private T setInitialValue() {
    T value = initialValue(); //protected方法,用户可以重写
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);   // 尝试根据当前线程作为key,找到map
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;    // 默认返回null
}

公有方法:(public): 仅有4个

set():

以当前线程为key,存放在Thread维护的ThreadLocalMap对象中。达到每个线程不一样的数据,达到隔离和线程安全的效果
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);  // 获取与当前线程维护的ThreadLocalMap对象
    if (map != null)
        map.set(this, value);         // 调用ThreadLocalMap的Set方法进行保存
    else   // 当前线程的map缓存副本不存在,则创建一个ThreadLocalMap
        createMap(t, value);
}

get():

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {               // Thread维护的ThreadLocalMap有值。
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();  // 返回一个默认的值,这个值默认为空
}

remove():

// 使用完成后,需要remove处理
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);     // 调用ThreadLocalMap对象的remove方法进行移除。
}

withInitial():

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

 

 

ThreadLocalMap源码分析:(JDK1.8)

ThreadLocalMap: ThreadLocal的一个静态内部类。每个线程中都有一个独立的ThreadLocalMap副本。它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,实现了变量访问在不同线程中的隔离。因此每个线程都有独有的变量,不会有并发错误

ThreadLocalMap源码分析:

static class ThreadLocalMap {

内部类:

ThreadLocalMap中的Entry类的key就是弱引用的,弱引用会在垃圾收集器工作的时候进行回收,只要执行垃圾回收,这些对象就会被回收
/**
 * 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;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

属性:

/*** The initial capacity -- MUST be a power of two. */
private static final int INITIAL_CAPACITY = 16;
/** The table, resized as necessary. table.length MUST always be a power of two. */
private Entry[] table;
/*** The number of entries in the table. */
private int size = 0;
/*** The next size value at which to resize. */
private int threshold; // Default to 0

构造方法:

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);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

内部方法:(全私有)

setThreshold

/*** Set the resize threshold to maintain at worst a 2/3 load factor.*/
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

nextIndex

/** * Increment i modulo len.*/
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

prevIndex

/** * Decrement i modulo len. */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

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();
        if (k == key) {
            e.value = value;
            return;
        }
// key为空的话,value这里的内存会被删除掉
        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;
    // Back up to check for prior stale entry in current run. We clean out whole runs at a time to avoid continual incremental rehashing due to garbage collector freeing up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    // Find either the key or trailing null slot of run, whichever occurs first
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // If we find key, then we need to swap it with the stale entry to maintain hash table order. The newly stale slot, or any other stale slot encountered above it, can then be sent to expungeStaleEntry to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            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.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    // If key not found, put new entry in stale slot
    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(expungeStaleEntry(slotToExpunge), len);
}

getEntry方法

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)     // 如果key为null,表明当前ThreadLocal以及被回收,此时需要delete掉entry
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

remove方法

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

expungeStaleEntry方法

// 根据索引清空entry
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;   // table数量-1
    // Rehash until we encounter null  (重新排列)
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;     // 结束条件,不为末尾
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {         // 如果key为null,则释放entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            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;
}

注意:使用ThreadLocal一般都是声明在静态变量中,如果不断地创建ThreadLocal而没有调用其remove方法,则会导致内存泄漏

为了避免内存泄漏,需要在不使用ThreadLocal时remove掉

ThreadLocal应用场景:

1. 如在线程级别,维护session,维护用户登录信息userID(登陆时插入,多个地方获取)

2. 数据库的链接对象 Connection,可以通过ThreadLocal来做隔离避免线程安全问题

。。。。。

内存泄漏问题(ThreadLocal):

内存泄漏发生场景:

ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用,如果一个ThreadLocal没有外部强引用,当系统执行GC时,这个ThreadLocal势必会被回收,这样ThreadLocalMap中就会出现一个key为null的Entry,而这个key=null的Entry是无法访问的,当这个线程一直没有结束的话,那么就会存在一条强引用链:Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value 永远无法回收而造成内存泄漏

ThreadLocalMap做了防护措施:

  • 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e
  • 如果e为null或key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询

在这个过程中遇到的keynullEntry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。

set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。

 

但这个设计一来与一个前提条件,就是调用get或者set方法,但是不是所有场景都会满足这个场景的,所以为了避免这类的问题,可在合适的位置手动调用ThreadLocalremove删除不需要的ThreadLocal,防止出现内存泄漏

 

避免内存泄漏的方案:

  • 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

 

 

Q:为什么要继承WeakReference,既然key可以被回收,但value不能被回收?

如果ThreadLocalMap的key不为弱引用,则set时为强引用,当ThreadLocal被置为null时,ThreadLocal还拥有一个强引用,就是这个key。此时,除非这个map强引用被消除,否则这个ThreadLocal的key内存空间将不会被回收。即使entry能被回收,key也不会被回收。

当继承WeekReference时,key强引用被去除后,只要有GC发生,这个key的空间都会被回收。

 

个人总结:

Thread类中有ThreadLocal.ThreadLocalMap的引用,在一个Java线程,栈中指向了堆内存中的一个ThreadLocal.ThreadLocalMap对象,此对象保存了若干个Entry,每个Entry的key(ThreadLocal实例)是弱引用,value是强引用(类似于WeakHashMap)。

 

用到弱引用的只是key,每个key都弱引用指向threadLocal,当把threadLocal实例置为null后,没有任何强引用指向threadLocal实例,所以threadLocal将会被gc回收,但是value却不能被回收,因为其还存在于ThreadLocal.ThreadLocalMap的对象的Entry之中。只有当前Thread结束之后,所有与当前线程有关的资源才会被GC回收。所以,如果在线程池中使用ThreadLocal,由于线程会复用,而又没有显示的调用remove的话的确是会有可能发生内存泄露的问题。

其实在ThreadLocal.ThreadLocalMap的get或者set方法中会探测其中的key是否被回收(调用expungeStaleEntry方法),然后将其value设置为null,这个功能几乎和WeakHashMap中的expungeStaleEntries()方法一样。因此value在key被gc后可能还会存活一段时间,但最终也会被回收,但是若不再调用get或者set方法时,那么这个value就在线程存活期间无法被释放。

其实ThreadLocal本身可以看成是没有内存泄露问题的,通过显示的调用remove方法即可

 

WeakReference是保证key能被回收的方案,ThreadLocalMap内部根据nullkey移除entry是保证value能被回收的方案

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值