浅析ThreadLocal的原理

举报
王小贰 发表于 2021/01/31 12:25:46 2021/01/31
【摘要】 1 ThreadLocal的作用当多个线程访问同一个共享变量的时候,开发人员必须采取措施避免并发操作所产生的各种冲突情况,有两种措施,锁同步及ThreadLocal。1.1 锁同步锁同步是指线程在访问共享变量前必须先获取锁资源,若获取锁资源失败就会被挂起,直至其他线程释放锁资源后,才被唤醒并再次尝试获取锁资源。通过锁同步机制,可以保证同一时间只有一个线程可以访问共享变量。获取锁资源获取锁资源...

1 ThreadLocal的作用

当多个线程访问同一个共享变量的时候,开发人员必须采取措施避免并发操作所产生的各种冲突情况,有两种措施,锁同步ThreadLocal

1.1 锁同步

锁同步是指线程在访问共享变量前必须先获取锁资源,若获取锁资源失败就会被挂起,直至其他线程释放锁资源后,才被唤醒并再次尝试获取锁资源。通过锁同步机制,可以保证同一时间只有一个线程可以访问共享变量

获取锁资源
获取锁资源
获取锁资源
访问
thread-1
锁同步
thread-2
thread-3
共享变量

我们可以通过synchronized关键字或Lock实现锁同步,以后再通过其他文章进行详细的介绍。

1.2 ThreadLocal

锁同步保证了同一时间只能有一个线程访问共享变量,而ThreadLocal则通过另一种思路解决并发导致的问题,那就是为每个线程提供了一个各自独享的本地变量,即各线程通过ThreadLocal变量访问各自的本地变量,因此无需锁同步保证线程安全。

访问
访问
访问
Thread-1访问
Thread-2访问
Thread-3访问
thread-1
ThreadLocal变量
thread-2
thread-3
thread-1的变量副本
thread-2的变量副本
thread-3的变量副本

1.3 锁同步与ThreadLocal对比

  • 锁同步:以牺牲时间效率为代价解决并发访问共享变量的冲突,因为,竞争锁资源会导致线程的上下文切换。

  • ThreadLocal:每个线程拥有自己的本地变量,不再需要考虑冲突的情况,但需要消耗更多的内存资源用于保存本地变量

2 ThreadLocal的使用例子

下面的例子,定义一个名为localStrThreadLocal变量;启动两个线程,分别使用localStr设置各自的变量值,并调用print()方法,print()方法会调用localStr.get()获取各线程的值,并输出。

public class ThreadLocalTest {
    private final static ThreadLocal<String> localStr = new ThreadLocal<>();

    /**
     * 输出localStr的值
     */
    private static void print() {
        // 打印变量
        System.out.println(Thread.currentThread().getName() + " - " + localStr.get());
        // 后面不再需要该本地变量了,把它remove掉
        localStr.remove();
    }

    public static void main(String[] args) {
        //
        // 创建线程1
        //
        new Thread(() -> {
            // 设置线程1的本地变量
            localStr.set("local value from thread-1");
            // 打印本地变量
            print();
            // print方法中,打印后会把本地变量移除,因此此处localStr.get()将返回null
            System.out.println("local value after thread-1 print() : " + localStr.get());
        }, "thread-1").start();

        //
        // 创建线程2
        //
        new Thread(() -> {
            // 设置线程2的本地变量
            localStr.set("local value from thread-2");
            // 打印本地变量
            print();
            // print方法中,打印后会把本地变量移除,因此此处localStr.get()将返回null
            System.out.println("local value after thread-2 print() : " + localStr.get());
        }, "thread-2").start();
    }
}

运行上面的代码,控制台将输出:

thread-1 - local value from thread-1
local value after thread-1 print() : null
thread-2 - local value from thread-2
local value after thread-2 print() : null
  • 1
  • 2
  • 3
  • 4

从上面的代码,可以看到ThreadLocal的两个常用方法,get()set(T value)remove()。上面的例子中:

(1) 定义了一个名为localStrThreadLocal类型变量。
(2) thread-1和thread-2都调用了localStr.set(Strint)方法设置各自的本地变量,最后的输出结果表明,不同的线程调用同一个ThreadLocal变量设置各自的本地变量并不会出现并发冲突的情况。
(3) 为了避免OOM异常,在本地变量使用完毕后,应该调用ThreadLocal 变量的remove()方法移除本地变量

3 TheadLocal的实现原理

要明白ThreadLocal的原理,需要先搞清楚 ThreadThreadLocalThreadLocalMap三者的作用和关系。下面的类图展示了三者的主要成员变量、方法及各自的关系:

ThreadLocal+set(value: T) : void +get() : T +remove() : void #getMap(t: Thread) : ThreadLocalMap #createMap(t: Thread, firstValue: T) : void -setInitialValue() : TThread#ThreadLocalMap threadLocals #ThreadLocalMap inheritableThreadLocalsThreadLocalMap

三者关系可以概括为

ThreadLocal并不存储实际的内容,它是一个工具类,各线程通过它,以ThreadLocal对象为key,要保存的值为value,把本地变量保存到各自的,类型为ThreadLocalMapthreadLocals成员变量中。下图展示了上面的例子是如何通过ThreadLocal保存各自的变量
在这里插入图片描述

3.1 Thread与ThreadLocalMap

从上面的类图,可知线程类Thread都包含了类型为ThreadLocalMap的两个变量,threadLocalsinheritableThreadLocals,上面说的线程的本地变量就是保存在它们里面,每个Thread都有属于自己的threadLocalsinheritableThreadLocals,互不相影响。下面为这两个ThreadLocalMap的定义代码段:

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

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

ThreadLocaMapThreadLocal里面的内部类,它的结构类似于HashMapThreadLocalMap是一个以ThreadLocalKeyMap,下面截取ThreadLocalMap的代码段:

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 table, resized as necessary.
	 * table.length MUST always be a power of two.
	 */
	private Entry[] table;

	...
}
27

从上面的代码段可知ThreadLocalMap的大概结构:

(1) 与HashMap类似,内部都是使用一个Entry数组保存数据。
(2) Entry是以ThreadLocal变量为Key。
(3) Entry继承了WeakReference,在Entry构造器中,通过调用super(k)ThreadLocal<?>变量保存到WeakReferencereferent变量中。WeakReference被称为弱引用,若一个对象,只被WeakReference引用了,那么,当发生gc的时候,该对象就会被回收。例如下面的代码:

public static void main(String[] args) {
	ThreadLocal<String> testTl = new ThreadLocal<>();   
	WeakReferenceTest test = new WeakReferenceTest(testTl);
	
	System.out.println(test.get());
	System.out.println(testTl);
	System.out.println("=====================================================");
	System.gc();
	
	// testTl = null; (1)
	Thread.sleep(10_000L);
	
	System.out.println(test.get());
	System.out.println(testTl);
}

static class WeakReferenceTest extends WeakReference<ThreadLocal<String>> {
    public WeakReferenceTest(ThreadLocal<String> referent) {
        super(referent);
    }
}

上面的代码,定义了WeakReference的子类WeakReferenceTest,并定义了一个名为testTlThreadLocal变量,并把它引用到WeakReferenceTest中。然后通过System.gc()强行触发gc,最后通过WeakReferenceTest.get()方法查看该ThreadLocal变量是否已被回收。

运行上面代码可看到下面输出,会发现该ThreadLocal变量并没有在gc时候被回收,这是因为它还被testTl引用着:

java.lang.ThreadLocal@25f38edc
java.lang.ThreadLocal@25f38edc
=====================================================
java.lang.ThreadLocal@25f38edc
java.lang.ThreadLocal@25f38edc

若我们把(1)的注释去掉,该ThreadLocal变量不再被testTl引用,而只被WeakReference引用了,因此,在gc时候就被回收:

java.lang.ThreadLocal@25f38edc
java.lang.ThreadLocal@25f38edc
=====================================================
null
null

因此,ThreadLocalMap中的Entry继承WeakReference的目的也非常明确了,就是若当作为KeyThreadLocal变量仅被Entry引用的时候,它就会在gc的时候被回收,当ThreadLocal被回收后,ThreadLocalMap中会存在keynullEntry,因此在执行getsetremove方法中,都会直接或间接调用expungeStaleEntry(int staleSlot)方法删除ThreadLocal已被回收的Entry

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;	// (1)
    tab[staleSlot] = null;
    size--;		// (2)

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);		 // (3)
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {	// (4)
            e.value = null;
            tab[i] = null;
            size--;
        } else {	// (5)
            int h = k.threadLocalHashCode & (len - 1);	// (6)
            if (h != i) {	// (7)
                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)	// (8)
                    h = nextIndex(h, len);
                tab[h] = e;		// (9)
            }
        }
    }
    return i;
}
  • (1) 把Entry[]数组中,staleSlot下标元素value设置为null,再把staleSlot为下标的元素设置为null
  • (2) 由清除了一个元素,因此size减1。
  • (3) 从staleSlot+1开始遍历Entry[]数组,目的是为了重新整理数组中元素的位置,使数组中的非空元素是连续保存的
  • (4) ThreadLocal已经被回收了,把对应的Entryvalue设置为null,并把Entry[]数组对应的下标元素设置为null。
  • (5) ThreadLocal未被回收。
  • (6) 通过ThreadLocalhashCodeEntry[]数组长度-1按位与计算对应的Entry所在的下标。注意:只有在数组长度为2的n次方才能使用这种算法,而ThreadLocalMap的长度永远都为2的n次方。
  • (7) 若第(6)步计算出的下标当前遍历到的下标不一致,则证明该Entry元素在保存到ThreadLocalMap的时候,发生了Hash冲突,即Entry[]数组中,对应计算出的下标的槽位,已被其他Entry占用了,因此要继续往后寻找第一个为null的位置保存进去。
  • (8) 从第(6)步计算出的下标开始,寻找第一个null元素下标,其目的是为了使Entry[]数组中的非空元素连续保存,避免了由于清除ThreadLocal被回收的Entry元素而导致数组的非空元素不连续。
  • (9) 把Entry保存到Entry[]数组第(8)步计算到的下标中。

3.2 ThreadLocal的作用

正如上节所说,线程的本地变量保存在Thread的变量threadLocalsinheritableThreadLocals中,下面先对threadLocals进行说明。ThreadLocal并没有保存任何内容,它作为一个工具类,通过getsetremove方法对ThreadthreadLocals进行获取、存放及删除操作。下面对ThreadLocal中的代码进行简要的分析。

3.2.1 void set(T value)

public void set(T value) {
    Thread t = Thread.currentThread();	// (1)
    ThreadLocalMap map = getMap(t);		// (2)
    if (map != null)					// (3)
        map.set(this, value);
    else
        createMap(t, value);			// (4)
}

  • (1) 获取当前的线程。
  • (2) 获取当前线程的threadLocals指向的ThreadLocalMap对象。
  • (3) 若threadLocals不为空,则以本ThreadLocal变量为keyvalue保存到threadLocals中。
  • (4) 若threadLocals为空,则创建,线程的threadLocals变量初始值null

下面再看getMap(Thread t)方法,返回的就是线程的threadLocals

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

下面再看createMap(Thread t, T firstValue)方法,为线程的threadLocals创建一个ThreadLocalMap对象。

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

3.2.2 T get()

public T get() {
    Thread t = Thread.currentThread();	// (1)
    ThreadLocalMap map = getMap(t);  // (2)
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);	// (3)
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();	// (4)
}
  • (1) 获取当前线程。
  • (2) 获取当前线程的threadLocals指向的ThreadLocalMap对象。
  • (3) 若threadLocals不为空,则以本ThreadLocal变量为key,获取value
  • (4) 若threadLocals为空,则初始化当前线程的threadLocals变量。

下面再看T setInitialValue()方法:

protected T initialValue() {
   return null;
}

private T setInitialValue() {
    T value = initialValue();	// (1)
    Thread t = Thread.currentThread();	// (2)
    ThreadLocalMap map = getMap(t);	// (3)
    if (map != null)	// (4)
        map.set(this, value);
    else		// (5)
        createMap(t, value);
    return value;	// (6)
}
  • (1) 执行initialValue()方法获取初始值,initialValue()方法只返回null
  • (2) 获取当前线程。
  • (3) 获取当前线程的threadLocals
  • (4) 如果threadLocals不为空,则以本ThreadLocalkeynull为值保存到threadLocals中。
  • (5) 如果threadLocals为空,则创建。
  • (6) 返回null

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。