12.ThreadLocal的那点小秘密

科技资讯 投稿 6500 0 评论

12.ThreadLocal的那点小秘密

王有志,一起聊技术,聊游戏,聊在外漂泊的生活。

ThreadLocal。

    什么是ThreadLocal?什么场景下使用ThreadLocal?
  • ThreadLocal的底层是如何实现的?
  • ThreadLocal在什么情况下会出现内存泄漏?
  • 使用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#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实例对象与执行线程的关系吗?

Thread.start0可以认为是操作系统层面的线程创建和启动。

\(Thread实例对象\approx执行线程\。也就是说,属于Thread实例对象的ThreadLocalMap也属于每个执行线程

属于线程。

Tips:

    实际上属于线程也即是属于Thread实例对象,因为Thread是线程在Java中的抽象;
  • ThreadLocalMap属于线程,但不代表存储到ThreadLocalMap的变量属于线程。

ThreadLocalMap的实现

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 = (Te.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对象并存储数据时,会为每个Thread对象创建ThreadLocalMap对象并存储数据,ThreadLocal对象作为Key。在每个Thread对象的生命周期内,都可以通过ThreadLocal对象访问到存储的数据。

到底是“谣言”吗?

我认为是的。ThreadLoal不会拷贝共享变量,它能“解决”并发安全问题的原理很简单,要求开发者为每个线程“发”一个变量,即变量本身就是线程隔离的。接近于以下写法:

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

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

Tips:Stack Overflow上也有关于“谣言”的讨论

最主要的功能就是跳过方法的参数列表在线程内传递参数。举个例子:Dubbo借鉴Netty的FastThreadLocal,搞了InternalThreadLocal,用来隐式传递参数。

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的情况:

避免内存泄漏

为了避免内存泄漏,Java建议设置静态ThreadLocal变量,保证一直存在与之关联的强引用

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

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

除此之外,开发者主动调用ThreadLocal#remove清除无用变量才是正确使用ThreadLocal的方式

ThreadLocal的注意事项

需要关注ThreadLocal的内存泄漏外,我们需要关注另外一种场景:线程池中使用ThreadLocal

ThreadLocal#remove的话,线程中会一直存在ThreadLocal关联的Value,那么就需要考虑清楚,这次的ThreadLocal对下一是否还适用?

结语

我也思考了“谣言”是如何产生的,大概有两点:

开发者自身保证了每个线程使用不同的DateFormat实例对象。

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


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

编程笔记 » 12.ThreadLocal的那点小秘密

赞同 (32) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽