彻底理解Java并发:ThreadLocal详解

Java
307
0
0
2022-12-20
标签   Java并发
本篇内容包括:ThreadLocal 简介、ThreadLocal 的使用(创建、方法、Demo)、ThreadLocal 原理、ThreadLocal 内存泄漏问题&使用时的注意事项以及其他 Thread 相关知识点(关于 ThreadLocal和Synchronized的区别、关于 ThreadLocalMap 中的 Hash 冲突处理)等内容。

一、ThreadLocal 简介

ThreadLocal 即线程变量,通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多个线程之间彼此并不独立,是共享变量。而 ThreadLocal 中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

Ps:ThreadLocal 很容易让人望文生义,想当然地认为是一个 “本地线程”。其实,ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量,也许把它命名为 ThreadLocalVariable 更容易让人理解一些。

ThreadLocal 为变量在每个线程中都创建了一个属于当前 Thread 的副本,且该副本只能由当前 Thread 使用,其它 Thread 不可访问,因此也就不存在多线程间共享的问题了。

ThreadLocal 变量通常被 private static 修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

在适用场景上 ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

二、ThreadLocal 的使用

1、创建方式

一般都会将 ThreadLocal 声明成一个静态字段,同时初始化如下

static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

其中 Object 就是原本堆中共享变量的数据。

2、常用方法

  • set(T value) – 设置线程本地变量的内容。
  • get() – 获取线程本地变量的内容。
  • remove() – 移除线程本地变量(值变为 null)。Ps:在线程池的线程复用场景中在线程执行完毕时一定要调用 remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。

3、Demo

既然 ThreadLocal 的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:

public static void main(String[] args) {
    // 新建一个ThreadLocal
    ThreadLocal<String> local = new ThreadLocal<>();
    // 新建一个随机数类
    Random random = new Random();
    // 使用 java8 的 Stream 新建 5 个线程
    IntStream.range(0, 5).forEach(a -> new Thread(() -> {
        // 为每一个线程设置相应的 local 值
        local.set(a + "  " + random.nextInt(10));
        System.out.println("线程和local值分别是  " + local.get());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start());
}
/*
		线程和local值分别是  0  4
		线程和local值分别是  2  8
		线程和local值分别是  3  9
		线程和local值分别是  1  4
		线程和local值分别是  4  4
*/

从结果我们可以看到,每一个线程都有各自的 local 值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的 local 值。

三、ThreadLocal 原理

1、ThreadLocal 原理概述

那么如何究竟是如何实现在每个线程里面保存一份单独的本地变量呢?

实际上线程在 Java 中就是一个 Thread 类的实例对象!而 Thread 的实例对象中实例成员字段的内容肯定是这个对象独有的,所以我们也可以将保存ThreadLocal线程本地变量作为一个Thread类的成员字段,这个成员字段就是:threadLocals。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

每个 Thread 维护着一个 ThreadLocalMap 的引用,而 ThreadLocalMap 又是 ThreadLocal 的内部类,用 Entry 来进行存储,ThreadLocal 创建的副本是存储在自己的 threadLocals 中的,也就是自己的 ThreadLocalMap:

  1. 每个 Thread 维护着一个 ThreadLocalMap 的引用
  2. ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储
  3. ThreadLocal 创建的副本是存储在自己的 threadLocals 中的,也就是自己的 ThreadLocalMap。
  4. ThreadLocalMap 的键值为 ThreadLocal 对象,而且可以有多个 threadLocal 变量,因此保存在 map 中
  5. 在进行 get 之前,必须先 set,否则会报空指针异常,当然也可以初始化一个,但是必须重写 initialValue() 方法。
  6. ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

2、ThreadLocalMap

ThreadLocalMap 是归 Thread 类所有的。它的引用在 Thread 类里,这也证实了一个问题:ThreadLocalMap 类内部为什么有 Entry 数组,而不是 Entry 对象?

因为你业务代码能 new 好多个 ThreadLocal 对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap 是同一个,而不是多个,不管你 new 几次 ThreadLocal,ThreadLocalMap 在一个线程里就一个,因为再说一次,ThreadLocalMap 的引用是在 Thread 里的,所以它里面的 Entry 数组存放的是一个线程里你 new 出来的多个 ThreadLocal 对象。

static class 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;

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

3、ThreadLocal 相关源码解析

ThreadLocal#set 方法的源码:

public void set(T value) {
    // 获取当前线程 
    Thread t = Thread.currentThread();
    // 获取当前线程的threadLocals字段 
    ThreadLocalMap map = getMap(t);
    // 判断线程的threadLocals是否初始化了 
    if (map != null) {
        map.set(this, value);
    } else {
        // 没有则创建一个ThreadLocalMap对象进行初始化
        createMap(t, value);
    }
}

ThreadLocal#createMap 方法的源码:

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

ThreadLocal.ThreadLocalMap#set 方法源码:

/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
    // map中就是使用Entry[]数据保留所有的entry实例
    Entry[] tab = table;
    int len = tab.length;
    // 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关 
    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;
        }
        if (k == null) {
            // 在设置期间清理哈希表为空的内容,保持哈希表的性质
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 扩容逻辑 
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocal#get 方法的源码:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 获取ThreadLocal对应保留在Map中的Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked") 
            // 获取ThreadLocal对象对应的值 
            T result = (T)e.value;
            return result;
        }
    }
    // map还没有初始化时创建map对象,并设置null,同时返回null 
    return setInitialValue();
}

ThreadLocal#remove 方法的源码:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 键在直接移除 
    if (m != null) {
        m.remove(this);
    }
}

四、ThreadLocal 内存泄漏问题

1、ThreadLocal 内存泄漏问题发生的原因

ThreadLocal 自身并不储存值,而是作为 一个 key 来让线程从 ThreadLocal 获取 value。而 Entry 是中的 key 是弱引用(Entry extends WeakReference<ThreadLocal<?>>),如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收。

并且,作为 ThreadLocalMap 的 key,ThreadLocal 被回收后,ThreadLocalMap 就会存在 key 为 null,但 value 不为 null 的 Entry(其实,在预防 ThreadLocal 内存泄漏问题上,Java 也做了一些努力:Java 在 Thread 中维护了 ThreadLocalMap,所以 ThreadLocalMap 的生命周期和 Thread(当前线程)一样长。并且,在 ThreadLocal 中,进行 get,set 操作的时候会清除 Map 里所有 key 为 null 的 value。)

但是,若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,或者分配(当前线程又创建了 ThreadLocal 对象)使用了又不再调用 get/set 方法,就可能引发内存泄漏。

其次,就算线程结束了,操作系统在回收线程或进程的时候不是一定杀死线程或进程的,在繁忙的时候,只会清除线程或进程数据的操作,重复使用线程或进程(线程 id 可能不变导致内存泄漏)。因此,key 弱引用并不是导致内存泄漏的原因,而是因为 ThreadLocalMap 的生命周期与当前线程一样长,并且没有手动删除对应 value。

2、为什么使用弱引用

通过对上述问题的分析我们可以发现,ThreadLocal 内存泄漏的一个主要原因就是 Entry 是中的 key 是弱引用,那这就有一个问题值得思考:为什么使用弱引用而不是强引用?

较为官方的说法是:为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

下面我们分两种情况讨论:

  1. key 使用强引用:引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。
  2. key 使用弱引用**:**引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。

比较两种情况,我们可以发现:

由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。

因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。

3、ThreadLocal 最佳实践

综合上面的分析,我们可以理解 ThreadLocal 内存泄漏的前因后果,那么怎么避免内存泄漏呢?

每次使用完 ThreadLocal,都调用它的 remove() 方法,清除数据。

在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。

五、Thread 相关知识点

1、关于 ThreadLocal和Synchronized的区别

相同点:ThreadLocal 和 Synchronized 都是为了解决多线程中访问相同变量的冲突问题。

不同点:

  • ThreadLocal:以空间换时间,为每个线程提供一个变量副本,消耗较多的内存,但是多个线程可以同时访问该变量而且相互不会影响。
  • Synchronized:以时间换空间,多个线程访问的是同一个变量,但是当多个线程同时访问该变量时,需要抢占锁,并且等待获取锁的线程释放锁,会消耗较多的时间。

2、关于 ThreadLocalMap 中的 Hash 冲突处理

ThreadLocalMap 作为一个 HashMap 和 ava.util.HashMap 的实现是不同的。对于 java.util.HashMap 使用的是链表法来处理冲突

但是,对于 ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放。