Java-ThreadLocal

ThreadLocal是一个线程内部的存储类,可以在指定的线程内存储数据,存储数据之后,只有指定的线程才能获取数据。

jdk中对ThreadLocal的注释如下:

This class provides thread-local variables.These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (eg. a user ID or Transaction ID).

ThreadLocal提供了线程内存储变量的能力。每一个线程都有自己独立的变量副本,通过get/set方法可以获取或者设置当前线程对应的值。ThreadLocal实例通常是类中的私有静态变量。常常将一些线程独有的数据或者状态与ThreadLocal关联起来。

用途:

  • 使用ThreadLocal代替Synchronized来保证线程安全
  • 使用ThreadLocal存储线程独有的数据

API如下:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);// 实际存储的数据结构类型
    if (map != null)// 如果存在map就直接set,没有则创建map并set
        map.set(this, value);
    else
        createMap(t, value);
}
void createMap(Thread t, T firstValue) {// 初始化线程对象中的ThreadLocalMap
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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;// thred中维护了一个ThreadLocalMap,返回thread的ThreadLocalMap
}
private T setInitialValue() {
    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;
}
protected T initialValue() {
    return null;
}

内存泄露

ThreadLocal操作不当会引起内存泄漏,根本原因在于Thread的静态内部类ThreadLocalMap中的Entry设计

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

Entry继承了WeakReference>,Entry的key是弱引用,所以key会在垃圾回收的时候被回收掉,而key对应的value则不会被回收,这样导致key没有值,value还存在的情况。

解决方法:每次用完ThreadLocal之后都调用它的remove方法清除数据。因为remove方法会主动将当前的key和value进行清除。

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 调用Reference#clear
            expungeStaleEntry(i);// 清除value
            return;
        }
    }
}

JDK开发者如何避免ThreadLocal的内存泄漏

// get -> getEntry -> getEntryAfterMiss -> expungeStaleEntry
private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)// 校验key
        return e;
    else// 对null值的key,擦除其value值
        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);// 清除value
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}// staleSlot 是已知的key=null的索引
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    tab[staleSlot].value = null;// 将value置null
    tab[staleSlot] = null;
    size--;
    Entry e;
    int i;// 遍历下一个key为null的entry,并将value指向null
    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 {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

关于其他方法

// set -> map.set -> cleanSomeSlots -> rehash -> expungeStaleEntries -> expungeStaleEntry
// remove -> map.remove -> expungeStaleEntry

最好的解决方法就是在使用完成之后手动调用remove方法。

如何手动释放ThreadLocal遗留存储的数据?

1、将remove方法提升为一个静态方法调用,内部去擦除每一个key为null的Entry。对于Spring的项目来说,可以在拦截器的afterCompletion方法中调用。这个方法就是在整个请求完成之后也就是渲染视图时候执行。

2、如果将Entry的key设置为强引用类型会存在什么问题?

如果 Entry的key设置为强引用类型,当threadlocal对象释放后,threadlocal=null。但这时线程私有的ThreadLocalMap中仍然强引用了threadlocal对象,这样导致threadLocal不能正常被GC回收

3、弱引用虽然会引起内存泄漏,但是在set、get、remove方法操作对null值的key进行擦除补救措施。

线程结束后不会自动清空Entry的value,当然实际上,如果当前线程执行完成之后,线程内的threadLocalMap对象处于不可达的状态,会被下一次GC回收,但是这种情况是在线程不被复用时才会出现。但是在项目中往往会使用线程池进行线程复用。

Spring对ThreadLocal的应用

spring使用了ThreadLocal来处理多线程下相同变量并发的线程安全问题。

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

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

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

  • 事务开始阶段
DataSourceTransactionManager#doBegin
->TransactionSynchronizationManager#bindResource
->TransactionSynchronizationManager#bindResource
  • 事务结束阶段
DataSourceTransactionManager#doCleanupAfterCompletion
-> TransactionSynchronizationManager#unbindResource
-> TransactionSynchronizationManager#unbindResource
-> TransactionSynchronizationManager#doUnbindResource

你可能感兴趣的:(Java-ThreadLocal)