33

都说 ThreadLocal 被面试官问烂了,可为什么面试官还是喜欢继续问

 4 years ago
source link: https://mp.weixin.qq.com/s/NFsKZpM1r7kME84Amq-nqQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

面试官 :有如下场景:在多线程并发情况下,有一个共享变量,不同线程设置不同值后,各线程只想获取自己设置的值,如何实现?

小小白 :使用ThreadLocal,通过ThreadLocal实例对象的set方法设置各线程自己的值,通过ThreadLocal实例对象的get方法获取各线程自己设置的值。

面试官 :写一下简单的实现代码?

小小白 :噼里啪啦写了如下代码。

public static void main(String[] args) {

final ThreadLocal threadLocal = new ThreadLocal();

threadLocal.set("hello");

System.out.println("main thread:" + threadLocal.get());

new Thread(new Runnable() {

@Override

public void run() {

threadLocal.set("world");

System.out.println("new Thread1:" + threadLocal.get());

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

threadLocal.set("!!!");

System.out.println("new Thread2:" + threadLocal.get());

}

}).start();

}

面试官 :ThreadLocal源码有看过吗?

小小白 :看过。

面试官 :那说一下ThreadLocal的实现原理?

小小白 :从ThreadLocal的set方法的源码开始:

public void set(T value) {

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null)

map.set(this, value);

else

createMap(t, value);

}

set方法的第一步是获取当前线程实例,然后通过getMap方法获取当前线程实例的threadLocals属性,threadLocals是ThreadLocalMap类型的变量,而ThreadLocalMap则是一个定制化的HashMap。   

// ThreadLocal类中的getMap方法

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

// Thread类中声明的threadLocals变量

ThreadLocal.ThreadLocalMap threadLocals = null;

如果getMap方法获取的ThreadLocalMap类型变量map不等于null,则以当前ThreadLocal实例对象为key,传入的value值为value存到这个ThreadLocalMap中,需要注意的是在实际存储的时候,key使用的是ThreadLocal的弱引用。

如果getMap方法获取的ThreadLocalMap类型变量map等于null,则调用createMap方法创建一个ThreadLocalMap实例对象,并以当前ThreadLocal实例对象为key,传入的value值为value存到这个ThreadLocalMap中。

看到这里就会明白,使用ThreadLocal时,每个线程维护一个ThreadLocalMap映射表,映射表的key是ThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。

紧接着进入ThreadLocal类的get方法,这个方法的第一步也是获取当前线程,通过getMap方法获取当前线程所维护的ThreadLocalMap实例,如果ThreadLocalMap对象实例不等于null,则以当前ThreadLocal对象实例为key从ThreadLocalMap中获取所需要的值。

public T get() {

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null) {

ThreadLocalMap.Entry e = map.getEntry(this);

if (e != null)

return (T)e.value;

}

return setInitialValue();

}

如果getMap方法获取到的ThreadLocalMap对象实例等于null,则调用setInitialValue方法初始化一个ThreadLocalMap,并以当前ThreadLocal实例对象为key,null值为value存到这个ThreadLocalMap中,同时返回null。

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);

return value;

}

protected T initialValue() {

return null;

}

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

void createMap(Thread t, T firstValue) {

t.threadLocals = new ThreadLocalMap(this, firstValue);

}

面试官 :看来这部分很熟悉了,那为什么Thread类中声明的threadLocals变量是map结构的?

小小白 :因为每个线程都可以关联很多个ThreadLocal变量。

面试官 :你知道ThreadLocal使用不当会发生内存泄露吗?

小小白 :知道。 每一个Thread维护一个ThreadLocalMap映射表,映射表的key是ThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。下面用一张图展示这些对象之间的引用关系,实心箭头表示强引用,空心箭头表示弱引用。

zeMNVvY.jpg!web

从上图可以看出,如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。 查看源码会发现,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。

面试官 :这个问题如何解决?

小小白 :解决办法是每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

面试官 :就你最开始写的代码,现在又有新需求,想在子线程中获取主线程 threadLocal中set方法 设置的值,如何实现?

小小白 :使用InheritableThreadLocal,ThreadLocal threadLocal = new InheritableThreadLocal(),这样在子线程中就可以通过get方法获取到主线程set方法设置的值了。

面试官 :InheritableThreadLocal是如何实现的?

小小白 :InheritableThreadLocal继承了ThreadLocal,并且重写了childValue、getMap和createMap方法,当在主线程中创建InheritableThreadLocal实例对象后,通过new Thread()方式创建子线程时,进入如下代码。

public Thread(Runnable target) {

init(null, target, "Thread-" + nextThreadNum(), 0);

}

private void init(ThreadGroup g, Runnable target, String name,

long stackSize) {

init(g, target, name, stackSize, null);

}

private void init(ThreadGroup g, Runnable target, String name,

long stackSize, AccessControlContext acc) {

if (name == null) {

throw new NullPointerException("name cannot be null");

}

this.name = name.toCharArray();

Thread parent = currentThread();

SecurityManager security = System.getSecurityManager();

if (g == null) {

if (security != null) {

g = security.getThreadGroup();

}

if (g == null) {

g = parent.getThreadGroup();

}

}

g.checkAccess();

if (security != null) {

if (isCCLOverridden(getClass())) {

security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);

}

}

g.addUnstarted();

this.group = g;

this.daemon = parent.isDaemon();

this.priority = parent.getPriority();

if (security == null || isCCLOverridden(parent.getClass()))

this.contextClassLoader = parent.getContextClassLoader();

else

this.contextClassLoader = parent.contextClassLoader;

this.inheritedAccessControlContext =

acc != null ? acc : AccessController.getContext();

this.target = target;

setPriority(priority);

// 如果父线程的inheritableThreadLocals不为null

if (parent.inheritableThreadLocals != null)

// 通过createInheritedMap方法将父线程中inheritableThreadLocals的值复制到子线程的inheritableThreadLocals中

this.inheritableThreadLocals =

ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

/* Stash the specified stack size in case the VM cares */

this.stackSize = stackSize;

/* Set thread ID */

tid = nextThreadID();

}

当使用InheritableThreadLocal创建实例对象时,当前线程Thread对象中维护了一个inheritableThreadLocals变量,它也是ThreadLocalMap类型,在创建子线程的过程中,将主线程维护的inheritableThreadLocals变量的值复制到子线程维护的inheritableThreadLocals变量中,这样子线程就可以获取到主线程设置的值了。 

推荐阅读:

Java注解是如何玩转的,面试官和我聊了半个小时

如何去除代码中的多次if而引发的一连串面试问题

String引发的提问,我差点跪了

就写了一行代码,被问了这么多问题

面试官:JVM对锁进行了优化,都优化了啥?

synchronized连环问

三分钟快速搞定git常规使用

MfUNZvm.jpg!web

点点" 在看 "


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK