一、应用场景举例
每个线程都应该有自己的数据库连接Connection,不能被其他线程所影响,就可以使用ThreadLocal(一个线程内各模块间共享同一数据,各线程间的数据又是独立的)。
二、使用举例
线程范围内的共享变量,每个线程有自己独立的数据。例如每个线程要有自己独立的连接。ThreadLocal本质上是一个map。
只要定义一个ThreadLocal变量,往这个变量里放的数据就是和线程相关的。一个ThreadLocal只能放一个变量,如果要存多个变量可以利用实体类。结合单例模式,把ThreadLocal定义在实体类。
public class ThreadTest4 { public static void main(String[] args){ for(int i=0;i<2;i++){ new Thread(new Runnable() { @Override public void run() { int data = new Random().nextInt(); System.out.println(Thread.currentThread().getName()+"has put data:" + data); Entity.getInstance().setData(data); //拿到本线程在这个类里对应的实例,再赋值 new A().get(); new B().get(); } }).start(); } } } class A { public void get(){ Entity mydata = Entity.getInstance(); System.out.println("A from "+Thread.currentThread().getName()+"has put data:" + mydata.getData()); } } class B { public void get(){ Entity mydata = Entity.getInstance(); System.out.println("B from "+Thread.currentThread().getName()+"has put data:" + mydata.getData()); } } //多变量实体类 class Entity{ private Entity(){} private static ThreadLocal<Entity> map = new ThreadLocal<Entity>(); //在这个类中定义一个ThreadLocal,存每个线程对应的实例 public static /*synchronized*/ Entity getInstance(){ //一般单例加synchronized会更严谨一点,这里取的就是本线程的实体类,所以可不加 Entity instance = map.get(); if(instance == null){ //如果这个线程还没有对应的实例,才往ThreadLocal存 instance = new Entity(); map.set(instance); } return instance; } int data; public int getData() { return data; } public void setData(int data) { this.data = data; } } 运行结果: Thread-0has put data:2114567887 Thread-1has put data:-746942766 A from Thread-1has put data:-746942766 A from Thread-0has put data:2114567887 B from Thread-0has put data:2114567887 B from Thread-1has put data:-746942766
三、源码分析
1、get()方法
//ThreadLocal的get()方法 public T get() { Thread t = Thread.currentThread(); //1、得到当前线程 ThreadLocalMap map = getMap(t); //2、从当前线程获得threadLocals成员变量 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); //3、从threadLocals得到对应线程数据 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } //2 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } //3 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); }
看完了get()方法,我发现ThreadLocal和我想象的不一样,我一开始以为ThreadLocal本身就是一个map数据结构,键是线程id,但其实真正存储数据的地方是在每个Thread对象的 threadLocals属性中,threadLocals 属性是一个 ThreadLocal.ThreadLocalMap 对象。并且通过ThreadLocal的threadLocalHashCode指定数据存储在数组的哪个位置下。
可能接下来就有疑问:ThreadLocalMap的数据结构是怎么样的?又是怎么存储数据的呢?
2、ThreadLocalMap数据结构
//Thread类的成员变量threadLocals public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; ...
//ThreadLocal.ThreadLocalMap 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; } } private static final int INITIAL_CAPACITY = 16; private Entry[] table; ...
ThreadLocal.ThreadLocalMap 是 ThreadLocal 的一个静态内部类。可以看出,ThreadLocalMap实际是使用一个数组 private Entry[] table 来存储数据,初始大小为16,类型为Entry。
对于Entry类型,一开始我看 Entry 只有一个value属性,就有一个疑问,为什么 Entry[] table 这个数组是一个map数组,而不是value数组。更仔细地查看源码后,发现Entry真的应该是一个键值对数组,并以 ThreadLocal<?>作为key,且这个key被弱引用类型包装 WeakReference<ThreadLocal<?>>。从ThreadLocalMap 的 getEntry() 方法可以看出进行了一次key的比对,而这里的key就是Entry的referent属性,且referent被一个弱引用包装。
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); } public T get() { return this.referent; }
综上,我们知道,ThreadLocal 并不是真正存放数据的数据结构,而是被弱引用包装,做为每个线程Thread对象中ThreadLocalMap类型属性的key。
为什么不是把ThreadLocal当成map,以线程ID为key这样的方式,而是以这样的方式来存放数据,其实思考一下,是能发现其可取之处的,对于后一种方式,当线程结束后,相关的数据会跟随一起被回收。如果把ThreadLocal当成map,理论上也能实现,但就可能会出现线程结束但它的相关资源仍存在的情况。
3、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); } private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //根据threadLocalHashCode找数组中对应的位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { //看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(); }
四、ThreadLocal内存泄漏的问题
根据上面Entry方法的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。JVM中弱引用在垃圾回收时,不管内存有没有占满,都会被GC回收。因此很有可能在某次GC之后,某个线程的某个ThreadLocal变量变成了null,那么在Entry中,Key也变成了null,在查找时将永远不会被找到,这个Entry的Value将永远不会被用到,这就是内存泄漏。
为了防止此类情况的出现,我们有两种手段。
1、手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露;
2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
参考链接:
http://www.cnblogs.com/digdeep/p/4510875.html
https://www.cnblogs.com/qiuyong/p/7091689.html
http://www.importnew.com/22039.html
https://blog.csdn.net/lhqj1992/article/details/52451136
https://blog.csdn.net/fly910905/article/details/78869251
https://www.cnblogs.com/xzwblog/p/7227509.html
https://www.cnblogs.com/coshaho/p/5127135.html
https://www.cnblogs.com/dolphin0520/p/3920407.html
http://blog.51cto.com/2179425/2082743
https://blog.csdn.net/woshiluoye9/article/details/72544764
https://blog.csdn.net/L_BestCoder/article/details/79252620