一文带你读懂ThreadLocal

1、在讲ThreadLocal之前先讲讲 ThreadLocal和Synchronized的联系与区别

联系:ThreadLocal和Synchronized都是为解决多线程对相同数据访问冲突的问题。

区别:

① Synchronized采用同步锁机制,使变量或代码块在同一时间只能被一个线程访问,采取的是“以时间换空间”的方式;

     ThreadLocal为每一个线程提供一份变量副本,使线程并行访问时操作数据互不影响,采取的是“以空间换时间”的方式;

② Synchronized保护的是多线程的共享变量,每一个线程对该变量的操作都会影响变量的最终结果,

     例如多线程操作火车票总数,每一个线程的操作都会影响最终的票数;

     ThreadLocal用于处理跟最终结果无关的变量,例如有一个静态变量

public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);

如果多个线程同时调用sdf.format(...),有可能导致sdf使用的内部数据结构被破坏,所以为每一个线程提供一个sdf的副本,每个线程使用各种的sdf,而使用多个sdf对最终结果并没什么影响。

 

2、下面就来详细讲讲ThreadLocal

public static final ThreadLocal threadLocal = new
    ThreadLocal() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

在程序中定义一个ThreadLocal类型的对象,使用变量threadLocal指向该对象,每个线程访问时,都会生成一个threadLocal对象副本,即使该线程调用多次方法,返回的还是同一个局部threadLocal对象。

 

2.1、ThreadLocal的get()方法

SimpleDateFormat sdf = threadLocal.get();

ThreadLocal的get()方法,就是从当前线程的ThreadLocalMap中取出当前线程对应的变量副本。该Map的key是ThreadLocal对象即上述threadLocal,value是当前线程对应的变量,即所需的sdf。

public T get() {
    Thread t = Thread.currentThread();
    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;
        }
    }
    return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

当第一次调用get()方法时,threadLocals变量尚未初始化,故get()方法会调用setInitalValue()方法

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

由于我们自己重写了initialValue()方法,故value值变为SimpleDateFormat对象,ThreadLocalMap没有被初始化的话,便初始化,并在构造时设置firstKey和firstValue;如果已经被初始化,那么将key和value存入map中。

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

注意:变量threadLocals是保存在线程Thread中的,而不是保存在ThreadLocal中。当前线程Thread中,有一个变量,引用名threadLocals,类型为ThreadLocal.ThreadLocalMap,这个引用是在ThreadLocal类中createmap方法内初始化的。

 

2.2、ThreadLocal的静态内部类ThreadLocalMap

 

每个线程都有这样一个名为threadLocals的ThreadLocalMap,以ThreadLocal(即threadLocal)和ThreadLocal对象泛型类型声明的变量(即SimpleDateFormat)作为key和value,这样,我们所使用的ThreadLocal变量的实际数据,通过get()方法取值的时候,就是通过取出当前Thread中threadLocals引用的map,然后从这个map中根据当前ThreadLocal作为参数,取出数据。现在,变量的副本从哪里取出的得到解决。

一文带你读懂ThreadLocal_第1张图片

ThreadLocalMap原理

static class ThreadLocalMap {
    static class Entry extends WeakReference> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
}

它也是一个类似于HashMap的数据结构,但是并没有实现Map接口。

也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,相应的线程被称为这些Entry的属主线程;只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set()方法,把ThreadLocal对象自己当做key,存入ThreadLocalMap中。

ThreadLocalMap的Entry继承了WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

 

ThreadLocalMap构造方法

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    // 表的大小始终为 2 的幂次
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //  设定扩容阈值
    setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

在ThreadLocalMap中,形如 key.threadLocalHashCode & (table.length - 1)(其中 key 为一个 ThreadLocal 实例)这样的代码片段实质上就是在求一个 ThreadLocal 对象的哈希值,只是在源码实现中没有将其抽为一个公用函数。

对于& (INITIAL_CAPACITY - 1),相对于 2 的幂作为模数取模,可以用&(2^n-1)来替代%2^n,位运算比取模效率高很多。至于为什么,因为对 2^n 取模,只要不是第n 位对结果的贡献显然都是 0,会影响结果的只能是第 n 位。

 

ThreadLocalMap何时初始化的

ThreadLocalMap是何时初始化的,上面有提及,是在get()方法的最后一行调用了setInitialValue()方法,而setInitialValue()中又调用createMap()方法对ThreadLocalMap进行初始化。

 

为什么ThreadLocalMap中Entry对key的引用是弱引用,对value的引用是强引用

Entry的key是一个ThreadLocal实例,value是一个线程特有对象。Entry的作用即是:为其属主线程建立起一个ThreadLocal实例与一个线程特有对象之间的对应关系;

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

如果key设置为强引用,当threadLocal实例释放后,threadLocal=null,但是Thread会有强引用指向threadLocalMap,而我们知道threadLocalMap中的key为threadLocal,那么threadLocalMap.Entry又强引用threadLocal,这样会导致threadLocal不能正常被GC回收。弱引用虽然会引起内存泄漏,但是有set()、get()、remove()等方法做补偿,对为null的key进行擦除,设计略胜一筹。

事实上,当currentThread执行结束后,threadLocalMap变得不可达而被回收,Entry等也被回收了,但这样就要求不对Thread进行复用,但是我们项目中经常会复用线程来提高性能,所以currentThread一般不会处于中止状态。

 
 

ThreadLocalMap#getEntry()方法 (由 ThreadLocal#get()方法调用)

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
        // 因为用的是线性探测,所以往后找还是有可能能够找到目标 Entry 的。
        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)
            // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的 entry
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

i 是位置

从 staleSlot 开始遍历,将无效 key(弱引用指向对象被回收)清理,即对应 Entry 中的 value置为 null,将指向这个 Entry 的 table[i]置为 null,直到遍历完 Entry。

另外,在过程中还会对非空的 Entry 作 rehash。

可以说这个函数的作用就是从 staleSlot 开始清理连续段中的 slot,断开value的强引用,rehash slot等。

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
    // Rehash until we encounter null
    Entry e;
    int i;
    // 遍历下一个key为空的Entry,并将value赋空
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        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;
                // 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;
}

 

ThreadLocalMap#set()方法 (线性探测法解决hash冲突)

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;
    //  计算 key 的 hash 值
    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 赋了新值,则替换原值为新值
            e.value = value;
            return;
        }
        if (k == null) {
            // 该位置的 TheadLocal 已经被回收,
            // 那么会清理 slot 并在此位置放入当前 key和 value(stale:陈旧的)
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //  下一个位置为空,那么就放到该位置上
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 启发式地清理一些 slot,并判断是否是否需要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal对象,hash 值就增加一个固定的大小 0x61c88647。

private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;

由于 ThreadLocalMap 使用线性探测法来解决散列冲突,所以实际上 Entry[]数组在程序逻辑上是作为一个环形存在的。

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

在插入过程中,根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置 i,过程如下:

1、如果当前位置是空的,那么正好,就初始化一个 Entry 对象放在位置 i 上;

2、不巧,位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 正好是即将设置的 key,那么重新设置 Entry 中的 value;

3、很不巧,位置 i 的 Entry 对象,和即将设置的 key 没关系,那么只能找下一个空位置;这样的话,在 get 的时候,也会根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置,然后判断该位置 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位置;

可以发现,set 和 get 如果冲突严重的话,效率很低,因为 ThreadLoalMap 是 Thread 的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。

 

cleanSomeSlots (启发式地清理  slot )

i 是当前位置,n 是元素个数
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 = 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;
}

 

ThreadLocalMap#rehash()方法

private void rehash() {
    //  进行一次全量清理
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    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)
            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; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
}

 

ThreadLocalMap#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();  // 显式断开弱引用,即清除key
            expungeStaleEntry(i);  // 进行段清理,即清除value
            return;
        }
    }
}

Reference#clear

public void clear() {
    this.referent = null;
}

 

2.3、ThreadLocal内存泄露问题

ThreadLocal操作不当会引发内存泄漏,主要原因在于其内部类ThreadLocalMap中Entry的设计。

Entry继承了WeakReference>,即Entry的key是弱引用,所以key会在垃圾回收的时候被回收掉,而value是强引用,不会被回收,这样会导致一种现象:key为null,value有值。

key为空的话,value是无效数据,久而久之,value的累加就会导致内存泄漏。

 

2.4、JDK的开发者是如何避免内存泄漏的

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

而且在一些方法中埋了对key=null的value擦除操作(但也未必会清理所有的需要被回收的 value),这样对应的 value 就没有 GC Roots可达了,下次 GC 的时候就可以被回收,如get()、set()、remove()。

这样做,也只能说尽可能的防止内存泄漏,但不能完全解决内存泄漏问题。比如极端情况下,我们只创建了ThreadLocal,但不调用set()、get()、remove()方法等。所以能解决问题的办法就是用完ThreadLocal之后手动调用remove()方法;

e.clear()用于清除Entry的key,它调用的是WeakReference中的方法:this.referent = null

expungeStaleEntry(i)用于清除Entry对应的value

 

3、拓展

Thread和ThreadLocal有什么联系

Thread和ThreadLocal是绑定的,ThreadLocal依赖于Thread去执行,Thread将需要隔离的数据存放到ThreadLocal中(准确的讲是ThreadLocalMap中),来实现多线程数据隔离处理。

 

手动释放ThreadLocal遗留存储?你怎么去设计/实现

这里主要是强化一下手动remove()的思想和必要性,设计思想与连接池类似。

包装其父类remove()方法为静态方法,如果是Spring项目,可以借助bean的生命周期,在拦截器的afterCompletion阶段进行调用。

 

Spring如何处理Bean多线程下的并发问题

ThreadLocal天生为解决相同变量的访问冲突问题。所以这个对于Spring的默认单例bean的多线程访问是一个完美的解决方案。Spring也确实是使用了ThreadLocal来处理多线程下相同变量的线程安全问题。

 

Spring如何保证数据库事务在同一个连接下执行的

要想实现jdbc事务,就必须是在同一个连接对象中操作,多个连接下的事务就会不可控,需要借助分布式事务完成,那Spring如何保证数据库事务在同一个连接下执行呢?

DataSourceTransactionManager是Spring的数据源事务管理器,它会在你调用getConnection()时从数据库连接池中获取一个connection,然后将其与ThreadLocal绑定,事务完成后解除绑定,这样就保证了事务在同一连接下完成。

 

 

 

 

你可能感兴趣的:(Java)