分享一下最近看的ThreadLocal的源码的一些体会。
/** * @author ATFWUS * @version 1.0 * @date 2021/11/8 21:23 * @description */ @Slf4j public class ThreadLocalTest { static ThreadLocal<String> localStr = new ThreadLocal<>(); public static void main(String[] args) { List<Thread> list = new LinkedList<>(); for(int i = 0; i < 1000; i++){ Thread t = new Thread(new Runnable() { @Override public void run() { localStr.set(Thread.currentThread().getName() + " localStr"); try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(localStr.get()); } }, "t" + String.valueOf(i)); list.add(t); } for (Thread t : list) { t.start(); } } }
而对于普通变量来说,很明显是存在线程安全问题的:
/** * @author ATFWUS * @version 1.0 * @date 2021/11/8 21:23 * @description */ @Slf4j public class ThreadLocalTest { static ThreadLocal<String> localStr = new ThreadLocal<>(); static String shareStr; public static void main(String[] args) { List<Thread> list = new LinkedList<>(); for(int i = 0; i < 1000; i++){ Thread t = new Thread(new Runnable() { @Override public void run() { shareStr = Thread.currentThread().getName() + " shareStr"; try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(shareStr); } }, "t" + String.valueOf(i)); list.add(t); } for (Thread t : list) { t.start(); } } }
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
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(); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
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; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
InheritableThreadLocal英语翻译一下就是可继承的ThreadLocal,让我们看下它和ThreadLocal的继承性体现在哪。
这里的继承性指的是:子线程是否能访问父线程的变量。
threadLocals是当前线程的成员变量,在子线程中不可见
/** * @author ATFWUS * @version 1.0 * @date 2021/11/9 14:29 * @description */ @Slf4j public class InheritableThreadLocalTest { static ThreadLocal<String> localStr = new ThreadLocal<>(); public static void main(String[] args) { localStr.set("main线程为其设置的值"); new Thread(new Runnable() { @Override public void run() { log.debug("访问localStr : " + localStr.get()); } }).start(); System.out.println(localStr.get()); } }
看一下InheritableThreadLocal的源码:
源码非常简短,下面简单分析一下:
从Thread的构造方法看,发现所有的构造方法都会调用init()方法进行初始化,init()方法有两个重载形式。
我们进入参数较多的init方法查看一下:
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; // 新线程还未创建出来,当前线程就是即将要创建线程的父线程 Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { /* Determine if it's an applet or not */ /* If there is a security manager, ask the security manager what to do. */ if (security != null) { g = security.getThreadGroup(); } /* If the security doesn't have a strong opinion of the matter use the parent thread group. */ if (g == null) { g = parent.getThreadGroup(); } } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkAccess(); /* * Do we have the required permissions? */ 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); // 如果父线程的inheritThreadLocals 不为空 if (inheritThreadLocals && parent.inheritableThreadLocals != null) // 设置子线程中的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(); }
我们重点看一下和inheritThreadLocals相关的地方(含注释的地方)
再看下ThreadLocal.createInheritedMap方法:
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
看下这个构造方法:
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { // 调用inheritThreadLocals重写的childValue方法 Object value = key.childValue(e.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++; } } } }
通过上面的源码分析,可以发现,InheritableThreadLocal的继承性主要体现在:创建子线程时,会将父线程的inheritThreadLocals里面所有entry拷贝一份给子进程。
那么当子进程被创建出来之后,父进程又修改了inheritThreadLocals里面的值,这个操作是否对子线程可见,通过上面的源码可知,这个操作明显是不可见的,下面有个demo可以证实。
/** * @author ATFWUS * @version 1.0 * @date 2021/11/9 14:29 * @description */ @Slf4j public class InheritableThreadLocalTest { static ThreadLocal<String> localStr = new ThreadLocal<>(); static InheritableThreadLocal<String> inheritableLocalStr = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { inheritableLocalStr.set("main线程第一次为inheritableLocalStr设置的值"); new Thread(new Runnable() { @Override public void run() { log.debug("子线程第一次访问inheritableLocalStr : " + inheritableLocalStr.get()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("子线程第二次访问inheritableLocalStr : " + inheritableLocalStr.get()); } }).start(); Thread.sleep(500); inheritableLocalStr.set("main线程第二次为inheritableLocalStr设置的值"); log.debug("main线程第二次为inheritableLocalStr赋值"); Thread.sleep(1000); } }
看下输出:
可以发现,子线程创建出来后,对父线程中inheritThreadLocals的修改操作,对子线程不可见。
要充分理解ThreadLocal中存在的内存泄露问题,需要有以下JVM对内存管理的前置知识(这里篇幅问题就不补充了):
在分析上述ThreadLocalMap源码的时候,注意到有一个小细节,ThreadLocalMap的Entry继承了WeakReference<ThreadLocal<?>>,也就是说Entry的key是一个对ThreadLocal<?>的弱引用。问题来了,为什么这里要使用弱引用呢?
现在假设Entry的key是一个对ThreadLocal的强引用,当ThreadLocal对象使用完后,外部的强引用不存在,但是因为当前线程对象中的threadLocals还持有ThreadLocal的强引用,而threadLocals的生命周期是和线程一致的,这个时候,如果没有手动删除,整个Entry就发生了内存泄露。
现在假设Entry的key是一个对ThreadLocal的弱引用,当ThreadLocal对象使用完后,外部的强引用不存在,此时ThreadLocal对象只存在Entry中key对它的弱引用,在下次GC的时候,这个ThreadLocal对象就会被回收,导致key为null,此时value的强引用还存在,但是value已经不会被使用了,如果没有手动删除,那么这个Entry中的key就会发生内存泄露。
使用弱引用还有一些好处,那就是,当key为null时, ThreadLocalMap中最多存在一个key为null,并且当调用set(),get(),remove()这些方法的时候,是会清除掉key为null的entry的。
通过这个方法的源码可以看出,key为null的那个entry实际上迟早会被替换成新的entry。
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; } // 发现key为空 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 static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
同理,可以看到在get方法中也存在:
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); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; // 替换过时的entry if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
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)]) { // 清除过时的key if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
ThreadLocal把数据存放到线程本地,解决了线程安全问题,没有使用锁,直接访问线程本地变量,效率较高(空间换时间。)
同时threadLocals的生命周期是和线程一致的,可以解决很多参数传递问题。
ATFWUS 2021-11-11