Java 类ThreadLocal的用法及实现原理

转载请注明原文地址:https://blog.csdn.net/yu749942362/article/details/107014198

1,ThreadLocal的使用

ThreadLocal提供线程局部变量。这些变量与它们的普通对应变量的不同之处在于,每个通过其getset方法访问变量的线程都有自己独立初始化的变量副本。实例通常是类中希望将状态与线程(例如,用户ID或事务ID)关联的私有静态字段。

每个线程都有一个对其线程局部变量副本的隐式引用,只要线程是活着的并且ThreadLocal实例是可访问的;在一个线程离开后,它的线程本地实例的所有副本都将受到垃圾收集的影响(除非存在对这些副本的其他引用)。

ThreadLocal的使用很简单,SDK暴露出可供调用的方法只有三个:在这里插入图片描述
除此之外还应关注一个重要的方法:initialValue

这四个方法的用处分别是:

  • initialValue:返回当前线程的线程局部变量的“初始值”。该方法将在线程第一次使用get方法访问变量时被调用,除非线程之前调用了set方法。通常,这个方法在每个线程中最多调用一次,但是在调用了remove之后再调用get,会再次调用该方法。

  • set:为此线程的线程局部变量设置指定值。

  • get:返回此线程局部变量的当前线程副本中的值。如果该变量没有值,则首先将其初始化为调用initialValue方法的返回值。

  • remove:删除此线程局部变量的当前线程值。

举个简单的例子,下面的类为每个线程生成一个本地的唯一名称标识。

public class ThreadLocalTest extends Thread {

    private static final int COUNT = 20;

    private static final AtomicInteger next = new AtomicInteger(0);
    private static final CountDownLatch countDownLatch = new CountDownLatch(COUNT);

    private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return next.incrementAndGet();
        }
    };


    @Override
    public void run() {
        setName("Thread:" + threadLocal.get());
        countDownLatch.countDown();
    }

    public static void main(String[] args) {
        List<ThreadLocalTest> list = new ArrayList<>(COUNT);
        for (int i = 0; i < COUNT; i++) {
            ThreadLocalTest threadLocalTest = new ThreadLocalTest();
            threadLocalTest.start();
            list.add(threadLocalTest);
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (ThreadLocalTest threadLocalTest : list) {
            System.out.println(threadLocalTest.getName());
        }
    }
}

观察一下运行结果
在这里插入图片描述
线程的Name在每次调用threadLocal.get()时被分配,并且在随后的调用中尽管next.incrementAndGet()已经发生改变,但threadLocal.get()始终保持不变。

2,ThreadLocal的实现

initialValuesetgetremove

2.1, 初始值 — initialValue()

源码给出的注释很详细:initialValue返回当前线程的线程局部变量的“初始值”。该方法通常只会在线程第一次使用get方法访问变量时被调用,除非线程之前调用了set方法,在这种情况下,不会为该线程调用initialValue方法。通常,每个线程最多调用该方法一次,但是在后续调用removeget时,可能会再次调用该方法。

protected T initialValue() {
	return null;
}

此方法只在方法private T setInitialValue()中调用,而setInitialValue也仅在get中被调用过。

initialValue的默认实现只返回了一个null。如果我们希望线程局部变量有一个非空的初始值,就需要重写initialValue方法(ThreadLocal子类化或使用匿名内部类重写此方法)。

2.2, 值的设置 — set(T value)

set(T value)的作用是将此线程局部变量的当前线程副本设置为指定值。

/**
 * 大多数子类不需要重写这个方法,只需要依赖{@link #initialValue}方法来设置线程局部变量的值。
 * @param value 存储在当前线程此线程的副本中的值—local。
 */
public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}

在getMap中只有一行代码,就是返回当前线程中的threadLocalsThreadLocalMap )对象。

这里引入了一个采用 线性探测法 来解决哈希冲突的哈希表实现——ThreadLocalMap。

线性探测法 的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说假如计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

ThreadLocalMap是一个定制的哈希映射,只适用于维护线程本地值。没有操作被导出到ThreadLocal类之外。这个类是包私有的,允许在类线程中声明字段。为了帮助处理非常大的和长期存在的使用,这个哈希表项对keys使用WeakReferences。但是,由于不使用引用队列,因此只有当表空间开始耗尽时,才保证删除陈旧的条目。

/**
 * 散列映射中的条目继承至WeakReference,使用其主ref字段作为键(始终是ThreadLocal对象)。
 * 注意,空键(即entry.get() == null)意味着不再引用该键,因此可以从表中删除该条目。
 * 这样的条目在下面的代码中称为“陈旧条目”。
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
	/** The value associated with this ThreadLocal. */
	Object value;

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

回到 set(T value) 方法。如果当前线程的threadLocals不为空,则调用其set(this, value)为当前线程的threadLocals设值为value;如果为空则调用createMap(t, value)为当前线程创建一个类型为ThreadLocalMapthreadLocals

常见解决哈希冲突时使用 拉链法线性探测法的较多,而HashMap中使用的则是拉链法。有兴趣的朋友可以看看《阅读HashMap源码时你可能会有这些疑问》。后文会简单介绍下ThreadLocalMap 中的线性探测法的实现。

2.3, 值的获取 — get()

/** 
 * 返回此线程局部变量的当前线程副本中的值。如果该变量没有当前线程的值,
 * 则首先将其初始化为initialValue方法返回的值。
 */
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();
}

首先获取当前线程的threadLocalsmap接收。如果map值不为空,则取其value并返回;如果map==null则调用setInitialValue()setInitialValue相比较于set(T value)只多出了一行,即首行调用了initialValue()。这刚好印证了“每个线程最多调用该方法一次”。

2.4, 值的移除 — remove()

获取当前线程的ThreadLocalMap并移除key为this的元素。

/**
 * 删除此线程局部变量的当前线程值。
 * 如果这个线程局部变量随后被当前线程get所读取,它的值将通过调用它的initialValue方法重新初始化,
 * 除非它的值在过渡期间被当前线程调用set。这可能导致在当前线程中多次调用initialValue方法。
 */
 public void remove() {
	 ThreadLocalMap m = getMap(Thread.currentThread());
	 if (m != null)
		 m.remove(this);
 }

3,ThreadLocalMap 之线性探测法

ThreadLocalMap中的set(ThreadLocal<?> key, Object value)方法为例,在阅读其源码之前先看ThreadLocal中的两个全局变量:

/**
 * 线程局部变量依赖于附加到每个线程的线性探测哈希映射(threadlocal和inheritablethreadlocal)
 * ThreadLocal对象充当键,通过threadLocalHashCode搜索。
 * 这是一个定制的哈希码(仅在ThreadLocalMaps中有用),它在一般情况下消除了冲突,
 * 即相同线程使用连续构造的线程局部变量,而在不太常见的情况下保持良好的行为。
 */
private final int threadLocalHashCode = nextHashCode();

/**
 * 下一个要给出的哈希码。从0开始,按照增量HASH_INCREMENT自增。
 */
private static AtomicInteger nextHashCode = new AtomicInteger();

threadLocalHashCode 是通过 nextHashCode 得到的。顾名思义,返回下一个哈希码。

private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}

HASH_INCREMENT = 0x61c88647 连续生成的哈希码之间的差异——将隐式顺序 thread-local的id转换为接近最优扩散的倍增哈希值,用于两级幂的表。至于为什么是这个值目前还不清楚。

/**
 * 设置与key关联的值
 */
private void set(ThreadLocal<?> key, Object value) {
	Entry[] tab = table;
	int len = tab.length;
	// 计算出key在table数组中的下标位置i,为什么这么写?可以看我的另一篇博文,下方有链接
	int i = key.threadLocalHashCode & (len-1);

	// 假如i位置在table中已经有值了,则调用nextIndex找寻i+1的位置,依次类推,直到找到一个为空的位置。
	// 然后将值放入其中,如果i+1超出了table的容量会折回下标为0的位置开始继续寻找。
	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;
	// 从下方代码可以看到,每次set之后都会检查map中元素数量是否超出阈值,
	// 如果超出阈值会调用rehash进行扩容,并通过setThreshold设置新的阈值,
	// 所以不用担心会因为一致找不到可用的位置而陷入死循环。
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

《阅读HashMap源码时你可能会有这些疑问》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值