ThreadLocal信任大家都有用过的,一般用作存取一些全局的信息。比方用户信息,流程信息,乃至在Spring框架里边经过事务注解Transactional去获取数据库衔接的完成上,也有它的一份劳绩。

ThreadLocal作为一个进阶必会知识点,而且仍是面试高频考点。网上博客对它的解读也必定不会少,可是网上博客解读水平良莠不齐,看多了难免会绕。不如自己亲身再梳理一遍,趁便记录下自己的解读。

ThreadLocal的线程阻隔性Demo

先来看一个小的demo

static ThreadLocal<Student> threadLocal = new ThreadLocal<Student>();
public static void main(String[] args) {
    threadLocalTest1();
}
private static void threadLocalTest1() {
    new Thread(new Runnable() {
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        System.out.println(threadLocal.get());
        }
    }).start();
    new Thread(new Runnable() {
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadLocal.set(new Student("zhangsan"));
        }
    }).start();
}

如代码所示,敞开两个线程。

第一个线程3秒之后去取在静态变量threadLocal里的变量。

第二个线程1秒之后去设置threadLocal里的变量。

这段代码运转的成果便是,第一个线程永久获取不到第二个线程给静态变量threadLocal里设置的变量。

定论:不同的线程操作同一个threadLocal目标,能够完成线程信息之间的阻隔。

猜测:看到set办法和get办法,大胆猜测threadLocal目标里边有个Map,key为当时线程,value为ThreadLocal泛型里的目标,这样就完成了在空间上的线程安全性。

但现实并不是这样,答案不在猜测中,而在源码中。

ThreadLocal.set()办法源码解读

public void set(T value) {
    // 获取当时线程
    Thread t = Thread.currentThread();
    // 获取当时线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 把当时ThreadLocal目标作为key,调用set办法的入参目标作为value,放入当时线程的ThreadLocalMap
        map.set(this, value);
    else
        // 经过当时线程和调用set办法的入参目标去结构Map
        createMap(t, value);
}

ThreadLocal.get()办法源码解读

public T get() {
    // 获取当时线程
    Thread t = Thread.currentThread();
    // 获取当时线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
	// 经过当时ThreadLocal目标去取Entry
	ThreadLocalMap.Entry e = map.getEntry(this);
	if (e != null) {
	    @SuppressWarnings("unchecked")
	    // 获取Entry的value返回
	    T result = (T)e.value;
	    return result;
	}
    }
    return setInitialValue();
}

那么ThreadLocalMap又是什么呢?

再点进源码,你会发现,ThreadLocalMap是ThreadLocal的一个静态内部类,一起在Thread类下有一个成员变量ThreadLocals

ThreadLocal.ThreadLocalMap threadLocals = null

那咱们顺着逻辑再下去看看ThreadLocalMap的set办法用来干嘛的。

ThreadLocalMap.set()办法源码解读

private void set(ThreadLocal<?> key, Object value) {
	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;
		}
	}
	// 设置Entry
	tab[i] = new Entry(key, value);
	int sz = ++size;
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

其实咱们在知道了HashMap的底层原理完成的根底上去了解上述代码并不难,取Entry数组长度,哈希,与运算取模,设置Entry趁热打铁。

可是这儿需求留意的是,也是ThreadLocal最有特色的一点,是这个Entry并不是一般了解里的Entry,而是ThreadLocalMap里边的一个静态内部类而且承继了WeakReference

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

到此,是不是对几个带有Thread的名词弄的有点晕,休息一下,咱们先来看张图梳理一下ThreadLocal,ThreadLocalMap,与Thread的关系,如图:

深入浅出ThreadLocal

层层深化,这时候问题又来了,弱引证是啥?啥是弱引证?弱什么?什么引证?

WeakReference演示

public static void main(String[] args) {
    wrTest();
}
private static void wrTest() {
    WeakReference<Student> weakReference = new WeakReference<Student>(new Student("aaaa"));
    System.out.println(weakReference.get());
    System.gc();
    System.out.println(weakReference.get());
}

输出的成果为:

Student@1b6d3586
null

在这个demo里边,咱们能够很明晰的看到弱引证的特性:当JVM进行废物收回时,无论内存是否足够,都会收回被弱引证相关的目标。

当废物收回器执行一次之后,本来的弱引证相关的目标就为null了,它具有这样的特质,又有什么用呢?

这就得引出内存走漏的问题了。

内存走漏

使用ThreadLocal哪些情况会发生内存走漏?

调用ThreadLocal的set办法设置某个目标进去,后来这个目标收回不了。一朝一夕,影响程序运转速度,终究形成OOM。

为什么会收回不了?是因为废物收回器执行之后,CurrentThread依然运转的前提下,Entry[]一直存在,可是其中有些key由所以承继了WeakReference,在GC之后其get办法返回值便是null了,导致取不到Entry里边key.get()为null的KEY所对应的value,而这块value永久也拜访不到了。如图:

深入浅出ThreadLocal

使用ThreadLocal如何避免内存走漏?

把ThreadLocal定义为static,坚持单例,不被收回。

用完ThreadLocal,需求手动擦除对应的Entry节点信息,记得调用ThreadLocal的remove办法。

特别是在实践项目的场景下,大多数情况下线程都是交给线程池在管理。一个线程使命跑完,通常不会立即销毁,而是放在线程池里边等候下次使命的降临(有种说法说是在把线程放回线程池的过程中会擦除Thread下的ThreadLocal.ThreadLocalMap threadLocals信息,当然,这是线程池帮咱们做的)无论线程池是否帮咱们擦除,咱们用完ThreadLocal手动remove总是安全的。

附上remove的源码。

public void remove() {
	// 获取当时线程的ThreadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null){
		m.remove(this);
	}
}
private void remove(ThreadLocal<?> key) {
    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)]) {
         // 匹配key
         if (e.get() == key) {
    	 // 清除一个Entry元素
    	 e.clear();
    	 expungeStaleEntry(i);
    	 return;
        }
    }
}

总结

解读完ThreadLocal的源码,再回归到它的命名,了解又深了一个层次:

Thread + Local = 线程 + 本地 = 线程本地变量 = 把某个目标放在了线程本地。

文理不分家,无妨借用文科的思想打比方去了解它:

ThreadLocal目标就像一个详细的客观的目标,能够是某个话题,某部电影,某本书,乃至某个人。

而每个Thread就像一个人,读者,旁观者。

Thread对ThreadLocal的set操作和get操作,就别离对应是一个人对某个客观的目标进行设置片面形象和获取片面形象。即便是同一个目标,不同的人会对其有不同的片面形象并记录在自己的脑海里,在每个人来看这些形象都是合理的,无论你处在哪个上下文,总能快速获取到这个形象信息,而不会错乱。

在空间上保留目标的副本,经过空间换时间的思想,也就完成了ThreadLocal的线程安全性。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。