我想ThreadLocal这东西,你们或多或少都了解过一点,我在接触ThreadLocal的时候,以为这东西很神奇,在网上看了不少博客,也看了一些书,总以为有一个坎跨不过去,因此对ThreadLocal一直是只知其一;不知其二的,好在这东西在实际开发中毕竟用的很少,因此也就得过且过了。固然我说的“用的很少”,只是对于普通的上层业务开发而言,其实在不少框架中,都用到了ThreadLocal,甚至有的还对ThreadLocal作了进一步的改进。可是ThreadLocal也算是并发编程的基础,因此还真的有必要,也必需要好好研究下的。今天咱们就来好好看看ThreadLocal。编程
咱们知道在多线程下,操做一个共享变量,很容易会发生矛盾,要解决这问题,最好的办法固然是每一个线程都拥有本身的变量,其余的线程没法访问,所谓“没有共享,就没有伤害”。那么如何作到呢?ThreadLocal就这样华丽丽的登场了。数组
咱们先来看看简单的应用:多线程
public static void main(String[] args) { ThreadLocal threadLocal = new ThreadLocal(); threadLocal.set("Hello"); System.out.println("当前线程是:" + Thread.currentThread().getName()); System.out.println("在当前线程中获取:" + threadLocal.get()); new Thread(() -> System.out.println("如今线程是"+Thread.currentThread().getName()+"尝试获取:" + threadLocal.get())).start(); }
运行结果:并发
当前线程是:main 在当前线程中获取:Hello 如今线程是Thread-0尝试获取:null
运行结果很好理解,在主线程中往threadLocal 塞了一个值,只有在同一个线程下,才能够得到值,在其余线程就没法获取值了。框架
在咱们探究ThreadLocal以前,先让咱们思考一个问题,若是叫你来实现ThreadLocal,你会怎么作?
ThreadLocal的目标就在于让每一个线程都有只属于本身的变量。最直接的办法就是新建一个泛型类,在类中定义一个map,key是Long类型的,用来保存线程的id,value是T类型的,用来保存具体的数据。工具
set的时候,就获取当前线程的id,把这个做为key,往map里面塞数据;this
get的时候,仍是获取当前线程的id,把这个做为key,而后从map中取出数据。线程
就像下面这个样子:code
public class ThreadLocalTest { public static void main(String[] args) { CodeBearThreadLocal threadLocal = new CodeBearThreadLocal(); threadLocal.set("Hello"); System.out.println("当前线程是:" + Thread.currentThread().getName()); System.out.println("在当前线程中获取:" + threadLocal.get()); new Thread(() -> System.out.println("如今线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start(); } } class CodeBearThreadLocal<T> { private ConcurrentHashMap<Long , T> hashMap = new ConcurrentHashMap<>(); void set(T value) { hashMap.put(Thread.currentThread().getId(),value); } T get() { return hashMap.get(Thread.currentThread().getId()); } }
运行结果:对象
当前线程是:main 在当前线程中获取:Hello 如今线程是Thread-0尝试获取:null
能够看到运行结果和“正版的ThreadLocal”是如出一辙的。
咱们本身也写了一个ThreadLocal,看上去一点问题也没有,仅仅几行代码就把功能实现了,给本身鼓个掌。那正版的ThreadLocal是怎么实现的呢?核心应该和咱们写的差很少吧。遗憾的是,正版的ThreadLocal和咱们写的能够说彻底不同。
咱们如今看看正版的ThreadLocal是怎么作的。
public void set(T value) { Thread t = Thread.currentThread();//获取当前的线程 ThreadLocalMap map = getMap(t);//获取ThreadLocalMap if (map != null)//若是map不为null,调用set方法塞入值 map.set(this, value); else createMap(t, value);//新建map }
让咱们来看看getMap方法:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
getMap方法比较简单,直接返回了传进来的线程对象的threadLocals,说明threadLocals定义在Thread类里面,是ThreadLocalMap 类型的,让咱们看看threadLocals的定义:
public class Thread implements Runnable{ ThreadLocal.ThreadLocalMap threadLocals = null; }
看到这个定义,你们必定有点晕,咱们是跟着ThreadLocal的set方法进来的,怎么到了这里又回到ThreadLocal了,你们别着急,咱们再来看看ThreadLocalMap是什么鬼?
ThreadLocalMap是ThreadLocal的静态内部类,咱们的数据就是保存在ThreadLocalMap里面,更详细的说咱们的数据就保存在ThreadLocal类中的ThreadLocalMap静态内部类中的Entry[]里面。
让咱们把关系理一理,确实有点混乱,Thread类里面定义了ThreadLocal.ThreadLocalMap字段,ThreadLocalMap是TheadLocal的内部静态类,其中的Entry[]是用来保存数据的。这就意味着,每个Thread实例中的ThreadLocalMap都是独一无二的,又不相互干扰。等等,这不就揭开了ThreadLocal的神秘面纱了吗?原来ThreadLocal是这么作到让每一个线程都有本身的变量的。
若是你还不清楚的话,不要紧,咱们再来讲的详细点。在咱们实现的ThreadLocal中,是利用map实现数据存储的,key就是线程Id,你能够理解为key就是Thread的实例,value就是咱们须要保存的数据,当咱们调用get方法的时候,就是利用线程Id,你能够理解为利用Thread的实例去map中取出数据,这样咱们取出的数据就确定是这个线程持有的。好比这个线程是A,你传入了B线程的线程Id,也就是传入了B线程的Thread的实例就确定没法取出线程A所持有的数据,这点应该毫无疑问把。可是,在正版的ThreadLocal中,数据是直接存在Thread实例中的,这样每一个线程的数据就被自然的隔离了。
如今咱们解决了一个问题,ThreadLocal是如何实现线程数据隔离的,可是还有一个问题,也就是我初学ThreadLocal看了不少博客,仍然百思不得其解的问题,既然数据是保存在ThreadLocalMap中的Entry[]的,那么就表明能够保存多个数据,否则用一个普通的成员变量不就OK了吗,为何要用数组呢?可是ThreadLocal提供的set方法没有重载啊,若是先set一个“hello”,又set一个“bye”,那么“bye”确定会把“hello”给覆盖掉啊,又不像HashMap同样,有key和value的概念。这个问题真的困扰我好久,后面终于知道了缘由了,咱们能够new多个ThreadLocal呀,就像这样:
public static void main(String[] args) { ThreadLocal threadLocal1 = new ThreadLocal(); threadLocal1.set("Hello"); ThreadLocal threadLocal2 = new ThreadLocal(); threadLocal2.set("Bye"); }
这样一来,会发生什么状况呢?再次放出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); }
threadLocal1,threadLocal2都调用了set方法,尽管threadLocal1和threadLocal2是不一样的实例,可是它们在同一个线程啊,因此getMap获取的ThreadLocalMap是同一个,这样就变成了在同一个ThreadLocalMap保存了多个数据。
具体是怎么保存数据的,这个代码就比较复杂了,包括的细节太多了,我看的也不是很懂,只知道一个大概,咱们先来看看Entry的定义把:
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } }
Entry又是ThreadLocalMap的静态内部类,里面只有一个字段value,也就是说和HashMap是不一样的,没有链表的概念。
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; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
其中的细节有点多,看的有点迷糊,可是最关键的应该还算是看懂了。
public T get() { Thread t = Thread.currentThread();//获取当前线程 ThreadLocalMap map = getMap(t);//传入当前线程,获取当前线程的ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//传入ThreadLocal实例,获取Entry if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result;//返回值 } } return setInitialValue(); }
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); }
set方法和get方法都分析完毕了,咱们来作一个小总结。咱们在外面所使用的ThreadLocal更像是一个工具类,自己不保存任何数据,而真正的数据是保存在Thread实例中的,这样就自然的完成了线程数据的隔离。最后送上一张图,来帮助你们更好的理解ThreadLocal:
咱们再来看看Entry的定义:
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry继承了WeakReference,关于WeakReference是什么东西,不是本文的重点,你们能够自行查阅。WeakReference包裹了ThreadLocal,咱们再来看Entry的构造方法,调用了super(k),传入了咱们传进来的ThreadLocal实例,也就是ThreadLocal被保存到了WeakReference对象中。这就致使了一个问题,当ThreadLocal没有强依赖,ThreadLocal会在下一次发生GC时被回收,key是被回收了,可是value却没有被回收呀,因此就出现了Entry[]存在key为NULL,可是value不为NULL的项的状况,要想回收的话,可让建立ThreadLocal的线程的生命周期结束。可是在实际的开发中,线程有极大多是和程序同生共死的,只要程序不中止,线程就一直在蹦跶。因此咱们在使用完ThreadLocal方法后,最好要手动调用remove方法,就像这样:
public static void main(String[] args) { ThreadLocal<String> threadLocal = new ThreadLocal(); try { threadLocal.set("Hello"); threadLocal.get(); } finally { threadLocal.remove(); } }
别忘了,最好把remove方法放在finally中哦。
咱们仍是来看博客一开头的例子:
public static void main(String[] args) { ThreadLocal threadLocal = new ThreadLocal(); threadLocal.set("Hello"); System.out.println("当前线程是:" + Thread.currentThread().getName()); System.out.println("在当前线程中获取:" + threadLocal.get()); new Thread(() -> System.out.println("如今线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start(); }
运行结果:
当前线程是:main 在当前线程中获取:Hello 如今线程是Thread-0尝试获取:null
代码后面new出来Thread是由主线程建立的,因此能够说这个线程是主线程的子线程,在主线程往ThreadLocal set的值,在子线程中获取不到,这很好理解,由于他们并非同一个线程,可是我但愿子线程能继承主线程的ThreadLocal中的数据。InheritableThreadLocal出现了,彻底能够知足这样的需求:
public static void main(String[] args) { ThreadLocal threadLocal = new InheritableThreadLocal(); threadLocal.set("Hello"); System.out.println("当前线程是:" + Thread.currentThread().getName()); System.out.println("在当前线程中获取:" + threadLocal.get()); new Thread(() -> System.out.println("如今线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start(); }
运行结果:
当前线程是:main 在当前线程中获取:Hello 如今线程是Thread-0尝试获取:null
这样就让子线程继承了主线程的ThreadLocal的数据,说的更准确些,是子线程继承了父线程的ThreadLocal的数据。
那究竟是如何作到的呢?仍是看代码把。
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,而且重写了三个方法,当咱们首次调用InheritableThreadLocal的set的时候,会调用InheritableThreadLocal的createMap方法,这就建立了ThreadLocalMap的实例,而且赋值给inheritableThreadLocals,这个inheritableThreadLocals定义在哪里呢?和ThreadLocal的threadLocals同样,也是定义在Thread类中。当咱们再次调用set方法的时候,会调用InheritableThreadLocal的getMap方法,返回的也是inheritableThreadLocals,也就是把原先的threadLocals给替换掉了。
当咱们建立一个线程,会调用Thread的构造方法:
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
init方法比较长,我只复制出和咱们要探究的问题相关的代码:
Thread parent = currentThread(); if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
这篇博客到这里就结束了,东西仍是挺多的,可是都是挺重要的,特别是ThreadLocal的缘由和产生内存泄露的缘由和避免的方法。