北京网站营销与推广,wordpress电话,建设网站公司挖掘挖掘工具,名站在线一、ThreadLocal简介
多线程访问同一个共享变量的时候容易出现并发问题#xff0c;特别是多个线程对一个变量进行写入的时候#xff0c;为了保证线程安全#xff0c;一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同…一、ThreadLocal简介
多线程访问同一个共享变量的时候容易出现并发问题特别是多个线程对一个变量进行写入的时候为了保证线程安全一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法当我们在创建一个变量后如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal是JDK包提供的它提供线程本地变量如果创建一乐ThreadLocal变量那么访问这个变量的每个线程都会有这个变量的一个副本在实际多线程操作的时候操作的是自己本地内存中的变量从而规避了线程安全问题如下图所示 二、ThreadLocal简单使用
下面的例子中开启两个线程在每个线程内部设置了本地变量的值然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量代码如下所示
package test;public class ThreadLocalTest {static ThreadLocalString localVar new ThreadLocal();static void print(String str) {//打印当前线程中本地内存中本地变量的值System.out.println(str : localVar.get());//清除本地内存中的本地变量localVar.remove();}public static void main(String[] args) {Thread t1 new Thread(new Runnable() {Overridepublic void run() {//设置线程1中本地变量的值localVar.set(localVar1);//调用打印方法print(thread1);//打印本地变量System.out.println(after remove : localVar.get());}});Thread t2 new Thread(new Runnable() {Overridepublic void run() {//设置线程1中本地变量的值localVar.set(localVar2);//调用打印方法print(thread2);//打印本地变量System.out.println(after remove : localVar.get());}});t1.start();t2.start();}
}下面是运行后的结果 三、ThreadLocal的实现原理
下面是ThreadLocal的类图结构从图中可知Thread类中有两个变量threadLocals和inheritableThreadLocals二者都是ThreadLocal内部类ThreadLocalMap类型的变量我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下每个线程中的这两个变量都为null只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们后面我们会查看这两个方法的源码。除此之外和我所想的不同的是每个线程的本地变量不是存放在ThreadLocal实例中而是放在调用线程的ThreadLocals变量里面前面也说过该变量是Thread类的变量。也就是说ThreadLocal类型的本地变量是存放在具体的线程空间上其本身相当于一个装载本地变量的工具壳通过set方法将value添加到调用线程的threadLocals中当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止那么这个本地变量将会一直存放在他的threadLocals中所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。下面我们通过查看ThreadLocal的set、get以及remove方法来查看ThreadLocal具体实怎样工作的 1、set方法源码
public void set(T value) {//(1)获取当前线程调用者线程Thread t Thread.currentThread();//(2)以当前线程作为key值去查找对应的线程变量找到对应的mapThreadLocalMap map getMap(t);//(3)如果map不为null就直接添加本地变量key为当前定义的ThreadLocal变量的this引用值为添加的本地变量值if (map ! null)map.set(this, value);//(4)如果map为null说明首次添加需要首先创建出对应的mapelsecreateMap(t, value);
}在上面的代码中(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明)该方法代码如下
ThreadLocalMap getMap(Thread t) {return t.threadLocals; //获取线程自己的变量threadLocals并绑定到当前调用线程的成员变量threadLocals上
}
如果调用getMap方法返回值不为null就直接将value值设置到threadLocals中key为当前线程引用值为本地变量如果getMap方法返回null说明是第一次调用set方法前面说到过threadLocals默认值为null只有调用set方法的时候才会创建map这个时候就需要调用createMap方法创建threadLocals该方法如下所示
void createMap(Thread t, T firstValue) {t.threadLocals new ThreadLocalMap(this, firstValue);
}createMap方法不仅创建了threadLocals同时也将要添加的本地变量值添加到了threadLocals中。
2、get方法源码
在get方法的实现中首先获取当前调用者线程如果当前线程的threadLocals不为null就直接返回当前线程绑定的本地变量值否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中类似于set方法的实现都是判断当前线程的threadLocals变量是否为null是则添加本地变量这个时候由于是初始化所以添加的值为null否则创建threadLocals变量同样添加的值为null。
public T get() {//(1)获取当前线程Thread t Thread.currentThread();//(2)获取当前线程的threadLocals变量ThreadLocalMap map getMap(t);//(3)如果threadLocals变量不为null就可以在map中查找到本地变量的值if (map ! null) {ThreadLocalMap.Entry e map.getEntry(this);if (e ! null) {SuppressWarnings(unchecked)T result (T)e.value;return result;}}//(4)执行到此处threadLocals为null调用该更改初始化当前线程的threadLocals变量return setInitialValue();
}private T setInitialValue() {//protected T initialValue() {return null;}T value initialValue();//获取当前线程Thread t Thread.currentThread();//以当前线程作为key值去查找对应的线程变量找到对应的mapThreadLocalMap map getMap(t);//如果map不为null就直接添加本地变量key为当前线程值为添加的本地变量值if (map ! null)map.set(this, value);//如果map为null说明首次添加需要首先创建出对应的mapelsecreateMap(t, value);return value;
}3、remove方法的实现
remove方法判断该当前线程对应的threadLocals变量是否为null不为null就直接删除当前线程中指定的threadLocals变量
public void remove() {//获取当前线程绑定的threadLocalsThreadLocalMap m getMap(Thread.currentThread());//如果map不为null就移除当前线程中指定ThreadLocal实例的本地变量if (m ! null)m.remove(this);}4、如下图所示每个线程内部有一个名为threadLocals的成员变量该变量的类型为ThreadLocal.ThreadLocalMap类型类似于一个HashMap其中的key为当前定义的ThreadLocal变量的this引用value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中如果当前线程一直不消亡那么这些本地变量就会一直存在所以可能会导致内存溢出因此使用完毕需要将其remove掉。 四、ThreadLocal不支持继承性
同一个ThreadLocal变量在父线程中被设置值后在子线程中是获取不到的。threadLocals中为当前调用线程对应的本地变量所以二者自然是不能共享的
package test;public class ThreadLocalTest2 {//(1)创建ThreadLocal变量public static ThreadLocalString threadLocal new ThreadLocal();public static void main(String[] args) {//在main线程中添加main线程的本地变量threadLocal.set(mainVal);//新创建一个子线程Thread thread new Thread(new Runnable() {Overridepublic void run() {System.out.println(子线程中的本地变量值:threadLocal.get());}});thread.start();//输出main线程中的本地变量值System.out.println(mainx线程中的本地变量值:threadLocal.get());}
}五、InheritableThreadLocal类
在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的而InheritableThreadLocal类则可以做到这个功能下面是该类的源码
public class InheritableThreadLocalT extends ThreadLocalT {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三个方法。其中createMap方法在被调用当前线程调用set方法时得到的map为null的时候需要调用该方法的时候创建的是inheritableThreadLocal而不是threadLocals。同理getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。
下面我们看看重写的childValue方法在什么时候执行怎样让子线程访问父线程的本地变量值。我们首先从Thread类开始说起
private void init(ThreadGroup g, Runnable target, String name,long stackSize) {init(g, target, name, stackSize, null, true);
}
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;//(1)获取当前线程(父线程)Thread parent currentThread();//安全校验SecurityManager security System.getSecurityManager();if (g null) { //g:当前线程组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();elsethis.contextClassLoader parent.contextClassLoader;this.inheritedAccessControlContext acc ! null ? acc : AccessController.getContext();this.target target;setPriority(priority);//(2)如果父线程的inheritableThreadLocal不为nullif (inheritThreadLocals parent.inheritableThreadLocals ! null)//3设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocalsthis.inheritableThreadLocals ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);this.stackSize stackSize;tid nextThreadID();
}在init方法中首先(1)处获取了当前线程(父线程)然后2处判断当前父线程的inheritableThreadLocals是否为null然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量然后赋值给子线程。下面是createInheritedMap方法和ThreadLocalMap的构造方法
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)ThreadLocalObject key (ThreadLocalObject) e.get();if (key ! null) {//调用重写的方法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;}}}
}在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。
六、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题
1、基础概念
首先我们先看看ThreadLocalMap的类图在前面的介绍中我们知道ThreadLocal只是一个工具类他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals调用线程的成员变量也知道threadLocals是一个ThreadLocalMap类型的变量下面我们来看看ThreadLocalMap这个类。在此之前我们回忆一下Java中的四种引用类型相关GC只是参考前面系列的文章(JVM相关)
①强引用Java中默认的引用类型一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
②软引用简言之如果一个对象具有弱引用在JVM发生OOM之前即内存充足够使用是不会GC这个对象的只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用如果软引用所引用的对象被回收之后该引用就会加入到与之关联的引用队列中
③弱引用这里讨论ThreadLocalMap中的Entry类的重点如果一个对象只具有弱引用那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前当发生GC时候无论当前内存是否足够弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用如果弱引用的对象被垃圾回收期回收掉JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到当引用的对象呗回收掉之后再调用get方法就会返回null
④虚引用虚引用是所有引用中最弱的一种引用其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。不能通过get方法获得其指向的对象 2、分析ThreadLocalMap内部实现
上面我们知道ThreadLocalMap内部实际上是一个Entry数组我们先看看Entry的这个内部类
/*** 是继承自WeakReference的一个类该类中实际存放的key是* 指向ThreadLocal的弱引用和与之对应的value值(该value值* 就是通过ThreadLocal的set方法传递过来的值)* 由于是弱引用当get方法返回null的时候意味着坑能引用*/
static class Entry extends WeakReferenceThreadLocal? {/** value就是和ThreadLocal绑定的 */Object value;//kThreadLocal的引用被传递给WeakReference的构造方法Entry(ThreadLocal? k, Object v) {super(k);value v;}
}
//WeakReference构造方法(public class WeakReferenceT extends ReferenceT )
public WeakReference(T referent) {super(referent); //referentThreadLocal的引用
}//Reference构造方法
Reference(T referent) {this(referent, null);//referentThreadLocal的引用
}Reference(T referent, ReferenceQueue? super T queue) {this.referent referent;this.queue (queue null) ? ReferenceQueue.NULL : queue;
}在上面的代码中我们可以看出当前ThreadLocal的引用k被传递给WeakReference的构造函数所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候当前线程的ThreadLocalMap就会存放一个记录这个记录的key值为ThreadLocal的弱引用value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法如果这个时候别的地方还有对ThreadLocal的引用那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用是不会释放的就会造成内存泄漏。
考虑这个ThreadLocal变量没有其他强依赖如果当前线程还存在由于线程的ThreadLocalMap里面的key是弱引用所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。
总结 THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用在没有其他地方对ThreadLoca依赖ThreadLocalMap中的ThreadLocal对象就会被回收掉但是对应的不会被回收这个时候Map中就可能存在key为null但是value不为null的项这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。