多线程之所以让人头疼就是其对于临界区即共享变量的改变是难以预计的。
但正所谓没有买卖就没有伤害,在多线程世界里没有共享就没有伤害。局部变量存储在栈里,栈是线程私有的,所以局部变量就不存在多线程的安全性问题,那么除此之外还有没有是线程安全的存储方式呢。
当然有,那就是ThreadLocal即线程本地存储。spring的事务控制,动态数据源自定义注解的数据源切换都用到了ThreadLocal,那我们就来看看ThreadLocal到底是为什么可以做到线程隔离。
ThreadLocal threadLocal = new ThreadLocal();
public void fun(String[] args) {
threadLocal.set("123");
final Object o = threadLocal.get();
}
大家都知道ThreadLocal对象拥有两个核心成员方法,set和get,同一个线程内使用同一个ThreadLocal对象set一个值,就能get出这个值,set第二遍就会覆盖上一个值。不同的线程不会get到其他线程set的数据。到底是什么样的数据结构可以让成员变量threadLocal保证线程之间不会互相影响的呢。
set(value)的实际作用是set(currentThreadId,value),而get()的实际实现是get(currentThreadId),这样子ThreadLocal内部就维护了一个Map表,如下图所示
/**
* @author pp_x
* @email pp_x12138@163.com
* @Description
* @date 2022/11/20 19:43
*/
public class MyThreadLocal<T> {
Map<Thread, T> locals =
new ConcurrentHashMap<>();
// 获取线程变量
T get() {
return locals.get(
Thread.currentThread());
}
// 设置线程变量
void set(T t) {
locals.put(
Thread.currentThread(), t);
}
}
这样子,将线程唯一id作为map的key,就完全可以实现不同线程存的值不会相互影响了,完美!收工!

jdk内部的ThreadLocal实现真的是这样的吗,当然不是,那么到底是什么样的呢,点进源码不就知道了,下面就是ThreadLocal在jdk8下的get方法和set方法的实现
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
根据源码可以看到,方法内部获取了类型为ThreadLocalMap的变量,但是不同的是这个map的key并不是线程id而是当前ThreadLocal对象(this)。这也就证明我们的思考是错误滴,下面是jdk对于ThreadLocal的实现概念图,这边贴出来便于大家进行对比

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


threadLocal.set(value),就会获取当前线程内部的ThreadLocalMap,并将当前threadLocal的对象引用作为key存入map,这样的好处是,ThreadLocalMap的生命周期和Thread强关联,并且ThreadLocalMap里对于ThreadLocal的引用还是弱引用(WeakReference),当线程销毁后map的空间自然释放了。value,通过Entry的构造方法可以看出,Entry中的这个value即存放ThreadLocal.set(value)的value值。 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;
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;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}