深入理解Java并发编程之ThreadLocal

2022-11-13 13:11:21 编程 理解 并发

ThreadLocal简介

变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢?jdk中的ThreadLocal类正是为了解决这样的问题。

ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量,在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。在线程中,可以通过get()/set()方法来访问变量。ThreadLocal实例通常来说都是private static类型的,它们希望将状态与线程进行关联。这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

我们先通过一个例子来看一下ThreadLocal的基本用法:

public class ThreadLocalTest {
	static class MyThread extends Thread {
		private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
		
		@Override
		public void run() {
			super.run();
			for (int i = 0; i < 3; i++) {
				threadLocal.set(i);
				System.out.println(getName() + " threadLocal.get() = " + threadLocal.get());
			}
		}
	}
	
	public static void main(String[] args) {
		MyThread myThreadA = new MyThread();
		myThreadA.setName("ThreadA");
		
		MyThread myThreadB = new MyThread();
		myThreadB.setName("ThreadB");
		
		myThreadA.start();
		myThreadB.start();
	}
}

运行结果(不唯一):

ThreadA threadLocal.get() = 0
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2

虽然两个线程都在向threadLocal对象中set()数据值,但每个线程都还是能取出自己设置的数据,确实可以达到隔离线程变量的效果。

ThreadLocal源码解析

ThreadLocal常用方法介绍

  • get()方法:获取与当前线程关联的ThreadLocal值。
  • set(T value)方法:设置与当前线程关联的ThreadLocal值。
  • initialValue()方法:设置与当前线程关联的ThreadLocal初始值。

当调用get()方法的时候,若是与当前线程关联的ThreadLocal值已经被设置过,则不会调用initialValue()方法;否则,会调用initialValue()方法来进行初始值的设置。通常initialValue()方法只会被调用一次,除非调用了remove()方法之后又调用get()方法,此时,与当前线程关联的ThreadLocal值处于没有设置过的状态(其状态体现在源码中,就是线程的ThreadLocalMap对象是否为null),initialValue()方法仍会被调用。

initialValue()方法是protected类型的,很显然是建议在子类重载该函数的,所以通常该方法都会以匿名内部类的形式被重载,以指定初始值,例如:

public class ThreadLocalTest {
	public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return Integer.valueOf(1);
		}
	};
}

remove()方法:将与当前线程关联的ThreadLocal值删除。

实现原理

ThreadLocal最简单的实现方式就是ThreadLocal类内部有一个线程安全的Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。

JDK最早期的ThreadLocal就是这样设计的,但是,之后ThreadLocal的设计换了一种方式,我们先看get()方法的源码,然后进一步介绍ThreadLocal的实现方式:

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

get()方法主要做了以下事情:

1、调用Thread.currentThread()获取当前线程对象t;

2、根据当前线程对象,调用getMap(Thread)获取线程对应的ThreadLocalMap对象:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

threadLocals是Thread类的成员变量,初始化为null:


ThreadLocal.ThreadLocalMap threadLocals = null;

3、如果获取的map不为空,则在map中以ThreadLocal的引用作为key来在map中获取对应的value e,否则转到步骤5;

4、若e不为null,则返回e中存储的value值,否则转到步骤5;

5、调用setInitialValue()方法,对线程的ThreadLocalMap对象进行初始化操作,ThreadLocalMap对象的key为ThreadLocal对象,value为initialValue()方法的返回值。

从上面的分析中,可以看到,ThreadLocal的实现离不开ThreadLocalMap类,ThreadLocalMap类是ThreadLocal的静态内部类。每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样的设计主要有以下几点优势:

  • 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能;
  • 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

ThreadLocalMap源码分析

ThreadLocalMap是用来存储与线程关联的value的哈希表,它具有HashMap的部分特性,比如容量、扩容阈值等,它内部通过Entry类来存储key和value,Entry类的定义为:

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

Entry继承自WeakReference,通过上述源码super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。

分析到这里,我们可以得到下面这个对象之间的引用结构图(其中,实线为强引用,虚线为弱引用):

我们知道,弱引用对象在Java虚拟机进行垃圾回收时,就会被释放,那我们考虑这样一个问题:

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部关联的强引用,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就再无妨访问,但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。

该强引用链如下:

CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value

因此,只要这个线程对象被GC回收,那些key为null对应的value也会被回收,这样也没什么问题,但在线程对象不被回收的情况下,比如使用线程池的时候,核心线程是一直在运行的,线程对象不会回收,若是在这样的线程中存在上述现象,就可能出现内存泄露的问题。

那在ThreadLocalMap中是如何解决这个问题的呢?

在获取key对应的value时,会调用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,该方法源码如下:

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

通过key.threadLocalHashCode & (table.length - 1)来计算存储key的Entry的索引位置,然后判断对应的key是否存在,若存在,则返回其对应的value,否则,调用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,源码如下:

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);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

ThreadLocalMap采用线性探查的方式来处理哈希冲突,所以会有一个while循环去查找对应的key,在查找过程中,若发现key为null,即通过弱引用的key被回收了,会调用expungeStaleEntry(int)方法,其源码如下:

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

通过上述代码可以发现,若key为null,则该方法通过下述代码来清理与key对应的value以及Entry:

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;

此时,CurrentThread Ref不存在一条到Entry对象的强引用链,Entry到value对象也不存在强引用,那在程序运行期间,它们自然也就会被回收。expungeStaleEntry(int)方法的后续代码就是以线性探查的方式,调整后续Entry的位置,同时检查key的有效性。

在ThreadLocalMap中的set()/getEntry()方法中,都会调用expungeStaleEntry(int)方法,但是如果我们既不需要添加value,也不需要获取value,那还是有可能产生内存泄漏的。所以很多情况下需要使用者手动调用ThreadLocal的remove()函数,手动删除不再需要的ThreadLocal,防止内存泄露。若对应的key存在,remove()方法也会调用expungeStaleEntry(int)方法,来删除对应的Entry和value。

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

InheritableThreadLocal

InheritableThreadLocal继承自ThreadLocal,使用InheritableThreadLocal类可以使子线程继承父线程的值,来看一段示例代码:

public class ThreadLocalTest {
	private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return Integer.valueOf(10);
		}
	};
	
	static class MyThread extends Thread {
		@Override
		public void run() {
			super.run();
			System.out.println(getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
		}
	}
	
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
		
		MyThread myThread = new MyThread();
		myThread.setName("线程A");
		myThread.start();
	}
}

运行结果:

main inheritableThreadLocal.get() = 10

线程A inheritableThreadLocal.get() = 10

可以看到子线程成功继承了父线程的值。

父线程还可以设置子线程的初始值,只需要重写InheritableThreadLocal类的childValue(T)方法即可,将上述代码的inheritableThreadLocal 定义修改为如下方式:

private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return Integer.valueOf(10);
    }
    
    @Override
    protected Integer childValue(Integer parentValue) {
        return Integer.valueOf(5);
    }
};

运行结果为:

main inheritableThreadLocal.get() = 10

线程A inheritableThreadLocal.get() = 5

可以看到,子进程成功获取到了父进程设置的初始值。

使用InheritableThreadLocal类需要注意的一点是,如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那子线程获取的还是旧值。

线程中用来实现上述功能的ThreadLocalMap类变量为

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal类的实现很简单,主要是重写了ThreadLocal类的getMap(Thread)方法和createMap(Thread, T)方法,将其中操作的ThreadLocalMap变量修改为了inheritableThreadLocals,这里不再进一步叙述。

参考资料

高洪岩:《Java多线程编程核心技术》

ThreadLocal和synchronized的区别

到此这篇关于深入理解Java并发编程之ThreadLocal 的文章就介绍到这了,更多相关Java ThreadLocal 内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章