很多 Java 开发一般都是做中台较多,并发编程使用的不多。因此,对 ThreadLocal 不太熟悉,所以笔者这里想让大家了解它,知道它是用来干什么的。
ThreadLocal 是 Java 中一种线程封闭技术,它提供了一种线程本地变量的机制,使得每个线程都拥有一个独立的变量副本,这样可以避免多个线程访问同一个变量时产生的并发问题。
ThreadLocal 在工作中还是蛮常用的,笔者使用到的一些场景如下:
总的来说,当你需要和线程绑定的变量时,就可以考虑使用 ThreadLocal 啦!
至于线程安全问题,大家不妨想想我们平常说线程安全问题都是出现在什么场景?同一时间有两个或两个以上的线程对同一个变量进行修改,才有可能出现线程安全问题。但是使用 ThreadLocal,每个线程是独享自己的变量副本的,哪里还有线程安全问题呢?
这个上网一搜一大堆,笔者就说下注意事项好了,用完后一定要释放,避免内存泄漏,提供几个点给大家参考:
总之,要正确使用 ThreadLocal 并避免内存泄漏问题,需要注意适时清理、使用弱引用、避免存储过多数据、及时释放资源,并在使用线程池时特别小心。
下面是一个简单的示例代码:
public class ThreadLocalExample {
private static final ThreadLocal
在示例代码中,线程 workerThread 和 ThreadLocal 实例是一个怎样的关系呢?set 方法和 remove 方法都做了什么呢?为什么会有内存泄漏的情况呢?我们带着疑问一起往下看。
我们直接从源码开始分析 ThreadLocal。
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 通过当前线程获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
结合示例代码来看,这里是当前线程A在 main 方法中通过 threadLocal 实例调用 threadLocal.set 方法,而 set 方法会给当前线程创建一个 ThreadLocalMap(如果没有的话),并使用 threadLocal 实例作为 key。
它们的关系如下图:
这里应该分成两种情况看:无线程复用和有线程复用。
那为什么使用 set/get/remove 可以避免内存泄漏呢?因为 set/get 在根据当前线程找到对应 Entry 元素后(这里是刚好是碰到了 key==null 的 entry[i],碰不到是不会顺手释放旧 value 的。因此,最好还是使用完后调用 remove 释放),发现 key == null,就会调用java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry 释放引用,所以就不会发生内存泄漏了。这里就不再展示源码了,有兴趣的可以自己去看下。
上面看到 ThreadLocalMap 使用了 Hash,是不是马上就想到了哈希冲突呢?HashMap 遇到哈希冲突,在 key 不相同的情况下,会使用链表解决。但是 ThreadLocalMap 的 Entry 没有 next 指针,因此它明显不会采用链表,那么它是如何解决哈希冲突的呢?
请看 java.lang.ThreadLocal.ThreadLocalMap#set 源码,笔者添加了注释,可以看到是怎么解决哈希冲突的。
private void set(ThreadLocal> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
// 存在哈希冲突的话,会往下走,如果超过数组长度,就会回到0
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
if (k == key) {
// 找到存储自己的entry,更新value
e.value = value;
return;
}
if (k == null) {
// 因为 gc 导致 key 被回收了,这个 Entry 会被新的 Entry 取代(新的Entry的key和value就是这里的传参),旧的会被释放
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
到这里相信大家对 ThreadLocal 都有了一定的了解。有什么想交流可以留言或私信笔者。
最后,介绍一款软件开发工具。JNPF开发平台,很多人都用过它,它是功能的集大成者,任何信息化系统都可以基于它开发出来。原理是将开发过程中某些重复出现的场景、流程,具象化成一个个组件、api、数据库接口,避免了重复造轮子。因而极大的提高了程序员的生产效率。
官网:www.jnpfsoft.com/?csdn,如果你有闲暇时间,可以做个知识拓展。
这是一个基于Java Boot/.Net Core构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;集成了代码生成器,支持前后端业务代码生成,满足快速开发,提升工作效率;框架集成了表单、报表、图表、大屏等各种常用的Demo方便直接使用;后端框架支持Vue2、Vue3。
为了支撑更高技术要求的应用开发,从数据库建模、Web API构建到页面设计,与传统软件开发几乎没有差异,只是通过低代码可视化模式,减少了构建“增删改查”功能的重复劳动。