JavaSE——多线程:ThreadLocal详解

1.概念

ThreadLocal用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量,也就是说ThreadLocal可以为每个线程创建一个单独的变量副本,相当于线程的private static类型变量

2.ThreadLocal的简单使用

public class Test {
    private static String commStr;
    private static ThreadLocal<String> threadStr = new ThreadLocal<String>();
    public static void main(String[] args) {
        commStr = "main";
        threadStr.set("main");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                commStr = "thread";
                threadStr.set("thread");
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(commStr);
        System.out.println(threadStr.get());
    }
}
//thread
//main

从运行结果可以看出,对于 ThreadLocal 类型的变量,在一个线程中设置值,不影响其在其它线程中的值,也就是说 ThreadLocal 类型的变量的值在每个线程中是独立的

3.ThreadLocal实现

3.1.set(T value)方法

  • set(T value) 方法中,首先获取当前线程,然后在获取到当前线程的 ThreadLocalMap
  • 如果 ThreadLocalMap 不为 null,则将 value 保存到 ThreadLocalMap 中,并用当前ThreadLocal 作为 key;否则创建一个ThreadLocalMap 并给到当前线程,然后保存 value
  • ThreadLocalMap 相当于一个 HashMap,是真正保存值的地方

3.2.get()方法

  • 在 get() 方法中也会获取到当前线程的 ThreadLocalMap
  • 如果 ThreadLocalMap 不为 null,则把获取key 为当前 ThreadLocal 的值,否则调用setInitialValue() 方法返回初始值,并保存到新创建的 ThreadLocalMap中

3.3.ThreadLocalMap详解

在 set,get,initialValue 和 remove 方法中都会获取到当前线程,然后通过当前线程获取到ThreadLocalMap,如果 ThreadLocalMap 为null,则会创建一个 ThreadLocalMap,并给到当前线程

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

可以看到,每一个线程都会持有有一个ThreadLocalMap,用来维护线程本地的值:

public class Thread implements Runnable {
...
    ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

在使用 ThreadLocal 类型变量进行相关操作时,都会通过当前线程获取到 ThreadLocalMap 来完成操作,每个线程的 ThreadLocalMap 是属于线程自己的,ThreadLocalMap 中维护的值也是属于线程自己的。这就保证了ThreadLocal 类型的变量在每个线程中是独立的,在多线程环境下不会相互影响

3.3.1.构造方法

ThreadLocal 中当前线程的 ThreadLocalMap 为 null 时会使用 ThreadLocalMap 的构造方法新建一个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);
}

构造方法中会新建一个数组,并将将第一次需要保存的键值存储到一个数组中,完成一些初始化工作

3.3.2.存储结构

ThreadLocalMap 内部维护了一个哈希表(数组)来存储数据,并且定义了加载因子:

// 初始容量,必须是 2 的幂
private static final int INITIAL_CAPACITY = 16;
// 存储数据的哈希表
private Entry[] table;
// table 中已存储的条目数
private int size = 0;
// 表示一个阈值,当 table 中存储的对象达到该值时就会扩容
private int threshold;
// 设置 threshold 的值
private void setThreshold(int len) {
threshold = len * 2 / 3;
}复制代码

table 是一个 Entry 类型的数组,Entry 是 ThreadLocalMap 的一个内部类

3.3.3.存储结构

Entry 用于保存一个键值对,其中 key 以弱引用的方式保存:

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

3.3.4.保存键值对

调用 set(ThreadLocal key, Object value) 方法将数据保存到哈希表中:

private void set(ThreadLocal key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        // 计算要存储的索引位置
        int i = key.threadLocalHashCode & (len-1);
        // 循环判断要存放的索引位置是否已经存在 Entry,若存在,进入循环体
        for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {
            ThreadLocal k = e.get();
            // 若索引位置的 Entry 的 key 和要保存的 key 相等,则更新该 Entry 的值
            if (k == key) {
                e.value = value;
                return;
            }
            // 若索引位置的 Entry 的 key 为 null(key 已经被回收了),表示该位置的 Entry 已经无效,用要保存的键值替换该位置上的 Entry
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 要存放的索引位置没有 Entry,将当前键值作为一个 Entry 保存在该位置
        tab[i] = new Entry(key, value);
        // 增加 table 存储的条目数
        int sz = ++size;
        // 清除一些无效的条目并判断 table 中的条目数是否已经超出阈值
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash(); // 调整 table 的容量,并重新摆放 table 中的 Entry
}

首先使用 key(当前 ThreadLocal)的 threadLocalHashCode 来计算要存储的索引位置 i,threadLocalHashCode 的值由 ThreadLocal 类管理,每创建一个 ThreadLocal 对象都会自动生成一个相应的threadLocalHashCode 值,其实现如下:

// ThreadLocal 对象的 HashCode
private final int threadLocalHashCode = nextHashCode();
// 使用 AtomicInteger 保证多线程环境下的同步
private static AtomicInteger nextHashCode = new AtomicInteger();
// 每次创建 ThreadLocal 对象是 HashCode 的增量
private static final int HASH_INCREMENT = 0x61c88647;
// 计算 ThreadLocal 对象的 HashCode
private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
}

在保存数据时,如果索引位置有 Entry,且该 Entry 的 key 为 null,那么就会执行清除无效 Entry 的操作,因为Entry 的 key 使用的是弱引用的方式,key 如果被回收(即 key 为 null),这时就无法再访问到 key 对应的value,需要把这样的无效 Entry 清除掉来腾出空间。在调整 table 容量时,也会先清除无效对象,然后再根据需要扩容:

private void rehash() {
        // 先清除无效 Entry
        expungeStaleEntries();
        // 判断当前 table 中的条目数是否超出了阈值的 3/4
        if (size >= threshold - threshold / 4)
        resize();
}

3.3.5.获取Entry对象

取值是直接获取到 Entry 对象,使用getEntry(ThreadLocal key) 方法:

private Entry getEntry(ThreadLocal key) {
        // 使用指定的 key 的 HashCode 计算索引位置
        int i = key.threadLocalHashCode & (table.length - 1);
        // 获取当前位置的 Entry
        Entry e = table[i];
        // 如果 Entry 不为 null 且 Entry 的 key 和 指定的 key 相等,则返回该 Entry
        // 否则调用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法
        if (e != null && e.get() == key)
            return e;
        else 
            return getEntryAfterMiss(key, i, e);
}

因为可能存在哈希冲突,key 对应的 Entry 的存储位置可能不在通过 key 计算出的索引位置上,也就是说索引位置上的 Entry 不一定是 key 对应的 Entry,所以需要调用getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法获取:

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        // 索引位置上的 Entry 不为 null 进入循环,为 null 则返回 null
        while (e != null) {
            ThreadLocal k = e.get();
            // 如果 Entry 的 key 和指定的 key 相等,则返回该 Entry
             if (k == key)
                return e;
             // 如果 Entry 的 key 为 null (key 已经被回收了),清除无效的 Entry
            // 否则获取下一个位置的 Entry,循环判断
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
}

3.3.6.移除指定的Entry

private void remove(ThreadLocal key) {
        Entry[] tab = table;
        int len = tab.length;
        // 使用指定的 key 的 HashCode 计算索引位置
        int i = key.threadLocalHashCode & (len-1);
        // 循环判断索引位置的 Entry 是否为 null
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            // 若 Entry 的 key 和指定的 key 相等,执行删除操作
            if (e.get() == key) {
                // 清除 Entry 的 key 的引用
                e.clear();
                // 清除无效的 Entry
                expungeStaleEntry(i);
                return;
            }
        }
}

3.4.内存泄漏

在ThreadLocalMap的set(),get()和remove() 方法中,都有清除无效Entry的操作,这样做是为了降低内存泄漏发生的可能。Entry中的key使用了弱引用的方式,这样做是为了降低内存泄漏发生的概率,但不能完全避免内存泄漏。
假设Entry的key没有使用弱引用的方式,而是使用了强引用:由于 ThreadLocalMap 的生命周期和当前线程一样长,那么当引用 ThreadLocal 的对象被回收后,由于 ThreadLocalMap 还持有 ThreadLocal和对应 value 的强引用,ThreadLocal和对应的value是不会被回收的,这就导致了内存泄漏。所以Entry以弱引用的方式避免了ThreadLocal没有被回收而导致的内存泄漏,但是此时value仍然是无法回收的,依然会导致内存泄漏。
ThreadLocalMap已经考虑到这种情况,并且有一些防护措施:在调用ThreadLocal的get(),set()和 remove() 的时候都会清除当前线程 ThreadLocalMap 中所有 key 为null的value。这样可以降低内存泄漏发生 的概率,所以我们在使用ThreadLocal的时候,每次用完ThreadLocal都调用remove() 方法`,清除数据,防止内存泄漏。

猜你喜欢

转载自blog.csdn.net/LiLiLiLaLa/article/details/94407279