咱们好,我是王有志。重视王有志,一起聊技能,聊游戏,聊在外漂泊的生活。

好久不见,不知道咱们新年过得怎么样?有没有痛痛快快得放松?是不是还能收到许多压岁钱?好了,话不多说,咱们开端今天的主题:ThreadLocal

我收集了4个面试中呈现频率较高的关于ThreadLocal的问题:

  • 什么是ThreadLocal?什么场景下运用ThreadLocal?
  • ThreadLocal的底层是怎么完成的?
  • ThreadLocal在什么状况下会呈现内存走漏?
  • 运用ThreadLocal要注意哪些内容?

咱们先从一个“流言”开端,经过分析ThreadLocal的源码,测验纠正“流言”带来的误解,并回答上面的问题。

撒播已久的“流言”

许多文章都在说“ThreadLocal经过复制同享变量的办法处理并发安全问题”,例如:

12.ThreadLocal的那点小秘密

这种说法并不准确,很简单让人误解为ThreadLocal会复制同享变量。来看个比如:

private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
	for (int i = 0; i < 1000; i++) {
		new Thread(() -> {
            try {
	            System.out.println(DATE_FORMAT.parse("2023-01-29"));
            } catch (ParseException e) {
	            e.printStackTrace();
	        }
	    }).start();
	}
}

咱们知道,多线程并发拜访同一个DateFormat实例目标会发生严重的并发安全问题,那么参加ThreadLocal是不是能处理并发安全问题呢?修改下代码:

/**
 * 榜首种写法  
 */
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() {
	@Override
    protected DateFormat initialValue() {
        return DATE_FORMAT;
    }
};
public static void main(String[] args) throws InterruptedException {
	for (int i = 0; i < 1000; i++) {
		new Thread(() -> {
            try {
	            System.out.println(DATE_FORMAT_THREAD_LOCAL.get().parse("2023-01-29"));
            } catch (ParseException e) {
	            e.printStackTrace();
	        }
	    }).start();
	}
}

估计会有许多小伙伴会说:“你这么写不对!《阿里巴巴Java开发手册》中不是这么用的!”。把书中的用法搬过来:

/**
 * 第二种写法  
 */
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>() {
	@Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};

Tips:代码小改了一下~~

咱们来看两种写法的差别:

  • 榜首种写法,ThreadLocal#initialValue时运用同享变量DATE_FORMAT
  • 第二种写法,ThreadLocal#initialValue创立SimpleDateFormat目标

按照“流言”的描述,榜首种写法会复制DATE_FORMAT的副本提供给不同的线程运用,但从结果上来看ThreadLocal并没有这么做。

有的小伙伴可能会怀疑是由于DATE_FORMAT_THREAD_LOCAL线程同享导致的,但别忘了第二种写法也是线程同享的。

到这儿咱们应该能够猜到,第二种写法中每个线程会拜访不同的SimpleDateFormat实例目标,接下来咱们经过源码一探终究。

ThreadLocal的完成

除了运用ThreadLocal#initialValue外,还能够经过ThreadLocal#set增加变量后再运用:

ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));
System.out.println(threadLocal.get().parse("2023-01-29"));

Tips:这么写仅仅是为了展现用法~~

运用ThreadLocal十分简略,3步就能够完成:

  • 创立目标
  • 增加变量
  • 取出变量

无参结构器没什么好说的(空完成),咱们从ThreadLocal#set开端。

ThreadLocal#set的完成

ThreadLocal#set的源码:

public void set(T value) {,
	Thread t = Thread.currentThread();
	// 获取当时线程的ThreadLocalMap
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// 增加变量
		map.set(this, value);
	} else {
		// 初始化ThreadLocalMap
		createMap(t, value);
	}
}

ThreadLocal#set的源码十分简略,但却走漏出了不少重要的信息:

  • 变量存储在ThreadLocalMap中,且与当时线程有关;
  • ThreadLocalMap应该类似于Map的完成。

接着来看源码:

public class ThreadLocal<T> {
	ThreadLocalMap getMap(Thread t) {
		return t.threadLocals;
	}
	void createMap(Thread t, T firstValue) {
		t.threadLocals = new ThreadLocalMap(this, firstValue);
	}
}
public class Thread implements Runnable {
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

很明晰的展现出ThreadLocalMap与Thread的联系:ThreadLocalMap是Thread的成员变量,每个Thread实例目标都拥有自己的ThreadLocalMap

别的,还记得在关于线程你必须知道的8个问题(上)说到Thread实例目标与履行线程的联系吗?

假如从Java的层面来看,能够认为创立Thread类的实例目标就完成了线程的创立,而调用Thread.start0能够认为是操作系统层面的线程创立和发动。

能够近似的看作是:Thread实例目标≈履行线程Thread实例目标\approx履行线程。也就是说,归于Thread实例目标的ThreadLocalMap也归于每个履行线程

根据以上内容,咱们如同得到了一个特别的变量作用域:归于线程

Tips

  • 实际上归于线程也即是归于Thread实例目标,由于Thread是线程在Java中的抽象;
  • ThreadLocalMap归于线程,但不代表存储到ThreadLocalMap的变量归于线程。

ThreadLocalMap的完成

ThreadLocalMap是ThreadLocal的内部类,代码也不复杂:

public class ThreadLocal<T> {
	private final int threadLocalHashCode = nextHashCode();
	static class ThreadLocalMap {
		static class Entry extends WeakReference<ThreadLocal<?>> {
			Object value;
			Entry(ThreadLocal<?> k, Object v) {
				super(k);
				value = v;
			}
		}
		private Entry[] table;
		private int size = 0;
		private int threshold;
		private void setThreshold(int len) {
			threshold = len * 2 / 3;
		}
		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);
		}
	}
}

仅从结构和结构办法中现已能够窥探到ThreadLocalMap的特点:

  • ThreadLocalMap底层存储结构是Entry数组;
  • 经过ThreadLocal的哈希值取模定位数组下标;
  • 结构办法增加变量时,存储的是原始变量

很明显,ThreadLocalMap是哈希表的一种完成,ThreadLocal作为Key,咱们能够将ThreadLocalMap看做是“简版”的HashMap。

Tips

  • 本文不评论哈希表完成中处理哈希冲突,数组扩容等问题的办法;
  • 也不需求重视ThreadLocalMap#setThreadLocalMap#getgetEntry的完成;
  • 与结构办法相同,ThreadLocalMap#set中存储的是原始变量

到目前为止,无论是ThreadLocalMap#set仍是ThreadLocalMap的结构办法,都是存储原始变量,没有任何复制副本的操作。也就是说,想要经过ThreadLocal完成变量在线程间的阻隔,就需求手动为每个线程创立自己的变量

ThreadLocal#get的完成

ThreadLocal#get的源码也十分简略:

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

前面的部分很简单了解,咱们看map == null时调用的ThreadLocal#setInitialValue办法:

private T setInitialValue() {
	T value = initialValue();
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		map.set(this, value);
	} else {
		createMap(t, value);
	}
	if (this instanceof TerminatingThreadLocal) {
		TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
	}
	return value;
}

ThreadLocal#setInitialValue办法几乎和ThreadLocal#set相同,但变量是经过ThreadLocal#initialValue获得的。假如是经过ThreadLocal#initialValue增加变量,在榜首次调用ThreadLocal#get时将变量存储到ThreadLocalMap中。

ThreadLocal的原理

好了,到这儿咱们现已能够构建出对ThreadLocal比较完整的认知了。咱们先来看ThreadLocal,ThreadLocalMap和Thread三者之间的联系:

12.ThreadLocal的那点小秘密

能够看到,ThreadLocal是作为ThreadLocalMap中的Key的,而ThreadLocalMap又是Thread中的成员变量,归于每一个Thread实例目标。忘掉ThreadLocalMap是ThreadLocal的内部类这层联系,整体结构就会十分明晰。

创立ThreadLocal目标并存储数据时,会为每个Thread目标创立ThreadLocalMap目标并存储数据,ThreadLocal目标作为Key。在每个Thread目标的生命周期内,都能够经过ThreadLocal目标拜访到存储的数据。

到底是“流言”吗?

那么“ThreadLocal经过复制同享变量的办法处理并发安全问题”是“流言”吗?

我认为是的。ThreadLoal不会复制同享变量,它能“处理”并发安全问题的原理很简略,要求开发者为每个线程“发”一个变量,即变量自身就是线程阻隔的。接近于以下写法:

public static Date parseDate(String dateStr) throws ParseException {
	return new SimpleDateFormat("yyyy-MM-dd").parse(dateStr);
}

那这还能算是ThreadLocal去处理并发安全问题吗?

Tips:Stack Overflow上也有关于“流言”的评论。

既然不是处理同享变量并发安全问题的,那么ThreadLocal有什么用?我认为最主要的功用就是跳过办法的参数列表在线程内传递参数。举个比如:Dubbo借鉴Netty的FastThreadLocal,搞了InternalThreadLocal,用来隐式传递参数。

ThreadLocal的内存走漏

在ThreadLocalMap的源码中能够看到,Entry承继自WeakReference,并且会将ThreadLocal增加到弱引证队列中:

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

咱们知道,弱引证相关的目标只能存活到下一次GC。假如ThreadLocal没有相关任何强引证,只有Entry上的弱引证的话,发生一次GC后ThreadLocal就会被回收,就会存在ThreadLocalMap上相关Entry,但Entry上没有Key的状况:

12.ThreadLocal的那点小秘密

此时Value仍旧相关在ThreadLocalMap上,但无法经过惯例手段拜访,造成内存走漏。虽然线程毁掉后会开释内存,但在线程履行期间,始终有一块无法拜访的内存被占用。

避免内存走漏

为了避免内存走漏,Java建议设置静态ThreadLocal变量,确保一向存在与之相关的强引证

ThreadLocal instances are typically private static fields in classes.

别的,ThreadLocal自身也做了一些努力去铲除这些没有Key的Entry,如:

  • ThreadLocalMap#getEntry调用ThreadLocalMap#getEntryAfterMiss
  • ThreadLocalMap#set调用ThreadLocalMap#replaceStaleEntry

这些办法中都会测验铲除无用的Entry,仅仅触发条件较为严苛,实际作用较小。

除此之外,开发者自动调用ThreadLocal#remove铲除无用变量才是正确运用ThreadLocal的办法

ThreadLocal的注意事项

除了需求重视ThreadLocal的内存走漏外,咱们需求重视别的一种场景:线程池中运用ThreadLocal

一般线程池不会毁掉线程,因此在线程池中运用ThreadLcoal,且没有正确履行ThreadLocal#remove的话,线程中会一向存在ThreadLocal相关的Value,那么就需求考虑清楚,这次的ThreadLocal对下一是否还适用?

结语

ThreadLocal的内容到这儿就完毕了,运用办法,完成原理,包含内存走漏都仍是比较简略的。不过有一点比较难搞,由于有太多人去写“ThreadLocal经过复制同享变量的办法处理并发安全问题”,导致许多人认为这是ThreadLocal的核心功用,所以无法确认坐在对面的面试官是怎么了解ThreadLocal的。

我也考虑了“流言”是怎么发生的,大概有两点:

榜首,《阿里巴巴Java开发手册》中运用ThreadLocal处理了DateFormat的并发安全问题,表现上看是ThreadLocal的能力,实际上是开发者自身确保了每个线程运用不同的DateFormat实例目标

第二,ThreadLocal的注释中,说到了一句“independently initialized copy of the variable.”,搞得咱们认为ThreadLocal会复制同享变量给线程运用。

假如真的遇到了这样面试官,那只能”见人说人话“了。


好了,今天就到这儿了,Bye~~