在很多场景下,我们想要传递参数必须通过显式参数定义,但是方法栈层次更加深的时候,显的特别不优雅,例如传递用户信息到调用的方法中:
//controller传递用户信息到serviceA controller.serviceA(user); //继续向serviceB传递 serviceA.serviceB(user); //最终传递到dao层 serviceB.dao(user); 复制代码
每一个方法都要定义一个显式的用户参数,显得非常臃肿,有没有其他办法呢?
有人可能想到了定义一个公共的属性或者静态变量,但是这样会引发一个多线程共享变量线程不安全问题,所以必须对这个公共属性进行加锁控制。
一旦上锁,那效率可不是慢了一星半点,有没有更加高效的办法呢?这时候就要用到***ThreadLocal***了。
上面提到的同步变量,采取的是统一治理,而ThreadLocal采取的策略是分而治之。
用官方的话来说:ThreadLocal类用来提供 线程内部的局部变量 。这种变量在多线程环境下访问(通过get或set方法访问)时能 保证各个线程里的变量相对独立于其他线程内的变量 。ThreadLocal实例通常来说都是 private static
类型的,用于关联线程和线程的上下文。
简单的说:
ThreadLocal提供了一个线程内部的变量副本,这个变量只在单个线程内部共享,在该线程内可以方便的访问ThreadLocal变量副本。
多个线程间的TreadLocal变量副本互不影响。
ThreadLocal只存活在线程的生命周期内,随着线程消亡而消亡(也可以手动调用remove方法移除ThreadLocal变量)。
这样一来就优雅的解决了单个线程中不同方法传递参数的复杂度问题,因为是每个线程内部共享变量,也不存在多线程不安全的问题了。
需要注意的是:
ThreadLocal不是同步机制,不解决多线程下的线程安全问题。
ThreadLocal为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的并发访问冲突。因为每一个线程都拥有自己的变量副本,也就不存在多线程不安全的问题了。
我们上面提到的传递用户信息的问题可以通过ThreadLocal实现:
public final class UserUtil { private static ThreadLocal<User> userThreadLocal = new ThreadLocal(); public static User getUser() { return userThreadLocal.get(); } public static User setUser(User user) { return userThreadLocal.set(user); } public static void removeUser() { userThreadLocal.remove(); } } 复制代码
//设置User controller(){ UserUtil.setUser(user); } //获取User serviceA(){ UserUtil.getUser(); } serviceB(){ UserUtil.getUser(); } dao(){ UserUtil.getUser(); } 复制代码
ThreadLocal为每一个线程创建了一个独立的副本变量,那它到底怎么实现的呢?
从ThreadLocal源码中,可以得知如下信息:
通过调用***get***方法获取ThreadLocal对应的变量,***get***方法源码如下:
public T get() { //获取当前线程 Thread t = Thread.currentThread(); //根据线程获取ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //获取Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { T result = (T)e.value; return result; } } //设置并返回初始值 return setInitialValue(); } 复制代码
此方法先是获取了当前线程,再通过当前线程获取了一个***ThreadLocalMap*** ,如果ThreadLocalMap 为空则设置一个初始值并返回。如果获取的map不为空,则根据当前的***ThreadLocal***获取对应的value并返回。
根据这个方法我们可以获取以下几点信息:
ThreadLocal对应的值都是存在***ThreadLocalMap***中。
ThreadLocalMap是根据***当前线程***实例实例获取的。
如果***ThreadLocalMap***为NULL或者ThreadLocal没有对应值的情况下,返回初始值(setInitialValue方法)。
既然知道了ThreadLocal的值是存在***ThreadLocalMap***中,我们继续看一下 ThreadLocalMap map = getMap(t);
这一段代码的具体实现。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; } 复制代码
这个方法直接返回了,线程中的一个***threadLocals***属性,进一步跟踪可以发现在***Thread***类中定义了此属性。
public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; ... } 复制代码
通过源码我们已经非常清晰的知道, Thread***中保存了***ThreadLocalMap ,而***ThreadLocalMap***中保存了***ThreadLocal***对应的键值对,示意图如下:
上一小节我们已经分析了***get***方法的源码,接下来我们分析一下其他的几段主要源码。
set方法:
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); } 复制代码
set***方法的作用是将当前***ThreadLocal实例***作为key,且将对应的value形成键值对保存进***ThreadLocalMap***中。如果***ThreadLocalMap***还未被创建则创建一个新的***ThreadLocalMap 。
创建***ThreadLocalMap***, createMap(t, value)
方法具体实现如下。
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } 复制代码
***createMap***方法直接***new***了一个***ThreadLocalMap***对象,传入的参数时当前的***ThreadLocal***实例以及一个需要保存的变量值,跟踪进入***ThreadLocalMap***构造方法。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //创建Entry数组 table = new Entry[INITIAL_CAPACITY]; //计算元素位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //创建Entry table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } 复制代码
***ThreadLocalMap***构造方法使用初始长度***INITIAL_CAPACITY***创建了一个***Entry***数组,并计算了初始元素的下标。
保存***ThreadLocal键值对***到数组对应的位置之后,设置size为1,并且初始化扩容下限。
由此代码可知,所有的***ThrealLocal键值对***最终保存的位置是一个***Entry数组***,Entry类定义在***ThreadLocalMap***类中。
public class ThreadLocal<T> { ... static class ThreadLocalMap { ... //弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... } ... } 复制代码
由于Entry继承了***WeakReference***,ThreadLocal被定义为弱引用对象,所以只要没有强引用指向ThreadLocal,那么触发GC时不管内存是否足够,都会把ThreadLocal回收掉。
但是这个时候有个问题,如果ThreadLocal被回收掉,而对应***Entry***中的value没有被回收,那这个value将永远无法访问到了。当越来越多的ThreadLocal被回收,对应的产生越来越多value无法被回收,最终会造成内存泄漏。
有人会说,那设计成强引用不就行了吗?其实设计成强引用并没有效果,如果没有手动将引用设置成null,反而造成了key(ThreadLocal)和value都不被回收,最终也会造成内存泄漏。
设计成强引用还会造成value被手动设置成null,而key(ThreadLocal)一直不被回收的情况,同样会造成内存泄漏。
这就是为什么ThreadLocal被设计成弱引用对象,当value被手动回收,同时会把ThreadLocal也进行回收,这无疑多了一份保险。
上面的分析也是常常有人提起使用ThreadLocal之后,需要手动的调用***set()、get()、remove()***否则会发生内存泄漏。
我们可以看一下***remove()***是怎么处理这个问题的。
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)]) { if (e.get() == key) { //清除key e.clear(); //进一步清除 expungeStaleEntry(i); return; } } } 复制代码
***remove()***循环了table,并且调用了***e.clear()***用来清除key。
而接下来的***expungeStaleEntry***方法做了几件事情:
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //设置value和tab[staleSlot]为null tab[staleSlot].value = null; tab[staleSlot] = null; size--; Entry e; int i; //循环所有元素,并清除key==null的元素 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //清除key==null的元素 if (k == null) { e.value = null; tab[i] = null; size--; } else { //重新排列元素位置 int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; } 复制代码
同样的在ThreadLocal的***set()、get()***方法中最终也会调用***expungeStaleEntry***方法来清除key为null的值,此处就不再赘述了。
ThreadLocal是用来解决线程内的资源共享,并且做到***线程间资源隔离***,但是某些场景我们需要在子线程中访问主线程的资源,这是否能实现呢?当然可以,这个时候需要用到另外一个类: InheritabIeThreadLocal 。
在Thread源码中我们很容易注意到***threadLocals***属性下方有另外一个属性***inheritableThreadLocals***。
public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; //继承map ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; ... } 复制代码
这两个属性的类型都是***ThreadLocal.ThreadLocalMap***,而***inheritableThreadLocals***是被***InheritableThreadLocal***类所引用。
我们先看一下***InheritableThreadLocal***类的源码
public class InheritableThreadLocal<T> extends ThreadLocal<T> { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } } 复制代码
InheritableThreadLocal***继承了***ThreadLocal ,覆盖了***childValue、getMap、createMap***这三个方法。
getMap、createMap***由原来操作***t.threadLocals***变成了操作***t.inheritableThreadLocals ,而***childValue***方法会在***ThreadLocalMap***类创建继承Map中用到。
public class ThreadLocal<T> { ... //创建一个继承map static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); } ... //根据parentMap创建一个新的ThreadLocalMap,其中的元素key和value值相同 private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; //循环创建新的Entry for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); //过滤key等于null的值 if (key != null) { //childValue返回的值即是e.value Object value = key.childValue(e.value); //key,value值保持和父线程一致 Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } } ... } 复制代码
createInheritedMap***是根据传入的***parentMap***复制了一个新的***ThreadLocalMap ,过滤掉key等于null的值,其他的元素key和value值和父线程保持一致。
那线程是在什么时候创建继承map呢?当线程初始化时,会调用***Thread***类的***init***方法,而当我们指定***inheritThreadLocals***为***true***并且父类线程***inheritableThreadLocals***不等于null的时候的时候,线程则会创建继承map。
public class Thread implements Runnable { .... //绝大部分情况下调用本初始化方法 private void init(ThreadGroup g, Runnable target, String name, long stackSize) { //inheritThreadLocals 默认为 true init(g, target, name, stackSize, null, true); } //线程初始化 private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { ... //父类线程为创建本线程的运行线程 Thread parent = currentThread(); ... //创建继承map if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); ... } ... } 复制代码
这里有三点需要大家注意一下:
绝大部分情况下***inheritThreadLocals***默认为true。
父类线程为创建本线程的运行线程 Thread parent = currentThread()
,也就是当前运行线程创建了一个新的线程,这个时候还是运行线程在执行而新线程还没有初始化完毕,所以 parent = currentThread()
。
子线程的***inheritableThreadLocals***,是根据父线程的***inheritableThreadLocals***复制得来的,相当于把父线程***inheritableThreadLocals***值传递下去,这样就实现了子线程获取到父线程的值。
下面通过一个例子比较一下***ThreadLocal***和***InheritableThreadLocal***的实际使用。
package com.gavin.test; public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { //设置主线程的值 ThreadLocalTest.threadLocal.set("threadLocal-main"); //启用线程1 new Thread(() -> { //设置线程1的值 ThreadLocalTest.threadLocal.set("threadLocal-1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程1==" + ThreadLocalTest.threadLocal.get()); }).start(); //启用线程2 new Thread(() -> { //设置线程2的值 ThreadLocalTest.threadLocal.set("threadLocal-2"); System.out.println("线程2==" + ThreadLocalTest.threadLocal.get()); }).start(); System.out.println("主线程==" + ThreadLocalTest.threadLocal.get()); } } 复制代码
本段代码定义了一个***threadLocal***属性,并且在三个线程中都对这个属性进行了设值,输出结果如下:
主线程==threadLocal-main 线程2==threadLocal-2 线程1==threadLocal-1 复制代码
由此可以得出三个线程的threadLocal属性互不干扰,我们改变一下代码,看一下子线程是否能取到主线程的值。
package com.gavin.test; public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { ThreadLocalTest.threadLocal.set("threadLocal-main"); new Thread(() -> { //直接打印值 System.out.println("线程1==" + ThreadLocalTest.threadLocal.get()); }).start(); new Thread(() -> { //直接打印值 System.out.println("线程2==" + ThreadLocalTest.threadLocal.get()); }).start(); System.out.println("主线程==" + ThreadLocalTest.threadLocal.get()); } } 复制代码
运行结果如下:
线程1==null 主线程==threadLocal-main 线程2==null 复制代码
由结果可以得出子线程并不能获取主线程的threadLocal值,再次证明线程间的threadLocal相互隔离。
为了实现子线程访问主线程的值,我们尝试使用***inheritableThreadLocal***来实现。
package com.gavin.test; public class InheritableThreadLocalTest { private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { InheritableThreadLocalTest.inheritableThreadLocal.set("inheritableThreadLocal-main"); new Thread(() -> { System.out.println("线程1==" + InheritableThreadLocalTest.inheritableThreadLocal.get()); }).start(); new Thread(() -> { System.out.println("线程2==" + InheritableThreadLocalTest.inheritableThreadLocal.get()); }).start(); System.out.println("主线程==" + InheritableThreadLocalTest.inheritableThreadLocal.get()); } } 复制代码
运行结果如下:
线程1==inheritableThreadLocal-main 主线程==inheritableThreadLocal-main 线程2==inheritableThreadLocal-main 复制代码
由结果得出子线程得到了主线程的值,假如子线程的值修改了,会影响主线程或者其他子线程吗?
根据上面的源码分析,主线程会把自己的***inheritableThreadLocal***传递给子线程,子线程重新new Entry对象用来保存key和value,所以子线程修改不会影响主线程的值,也不会影响其他子线程,只会向自己的子线程传递,我们来验证一下吧。
package com.gavin.test; public class InheritableThreadLocalTest { private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { InheritableThreadLocalTest.inheritableThreadLocal.set("inheritableThreadLocal-main"); new Thread(() -> { InheritableThreadLocalTest.inheritableThreadLocal.set("inheritableThreadLocal-1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程1==" + InheritableThreadLocalTest.inheritableThreadLocal.get()); new Thread(() -> { System.out.println("线程1的子线程==" + InheritableThreadLocalTest.inheritableThreadLocal.get()); }).start(); }).start(); new Thread(() -> { InheritableThreadLocalTest.inheritableThreadLocal.set("inheritableThreadLocal-2"); System.out.println("线程2==" + InheritableThreadLocalTest.inheritableThreadLocal.get()); }).start(); System.out.println("主线程==" + InheritableThreadLocalTest.inheritableThreadLocal.get()); } } 复制代码
运行结果如下
主线程==inheritableThreadLocal-main 线程2==inheritableThreadLocal-2 线程1==inheritableThreadLocal-1 线程1的子线程==inheritableThreadLocal-1 复制代码
正如我们猜想的那样:
掘金专栏: juejin.im/user/5ba21d…
作者:GavinKing
原创不易,转载请取得作者同意,并带上版权信息,谢谢