ThreadLocal
1.理解ThreadLocal
*1.1早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出
优美的多线程程序。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它
线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
*1.2ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:* void set(Object value)设置当前线程的线程局部变量的值。* public Object get()该方法返回当前线程所对应的线程局部变量。* public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。* protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
*1.3ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本
2.案例实现
需求:SimpleDateFormat是线程不安全的类,但是在项目中我们需要这个类来进行日期的转换,如果有一个页面需要进行容器格式的转换,多个
人共同访问的话,默认用的是同一个SimpleDateFormat的话(写在容器转换类里面,用static修饰),会容易出现线程安全问题,如果每一个,如果这个SimpleDateFormat不进行共享的话,每次访问都创建对象的话,那么会创建太多的对象,内存消耗比较大
这时就有比较折中的方法,就是为每一个线程单独创建一个SimpleDateFormat,这样在同一个线程内部,用的是同一个SimpleDateFormat,但是在不同的线程,使用的SimpleDateFormat是不一样的
看代码实现:class DateUtils {/** SimpleDateFormet 1)线层不安全 2)此对象不能被多个线程共享(局部性能不好)* 3)可以将此独享设置为线程内部单例(内个线程有一份)*//** ThreadLocal对象提供了这样一种机制: 1)可以将某个对象绑定到当前线程 2)可以从某个线程获取某个对象*/private static ThreadLocal<SimpleDateFormat> td = new ThreadLocal<>();// 线程内部单例public static SimpleDateFormat getInstance() {// 1.从当前线程获取对象SimpleDateFormat sdf = td.get();if (sdf == null) {sdf = new SimpleDateFormat("yyyy-MM-hh");}td.set(sdf);return sdf;}}public class ThreadLocalDemo2 {public static void main(String[] args) {SimpleDateFormat s1 = DateUtils.getInstance();SimpleDateFormat s2 = DateUtils.getInstance();SimpleDateFormat s3 = DateUtils.getInstance();System.out.println("main:" + (s1 == s2));System.out.println("main:" + (s2 == s3));new Thread(() -> {SimpleDateFormat s5 = DateUtils.getInstance();SimpleDateFormat s6 = DateUtils.getInstance();SimpleDateFormat s7 = DateUtils.getInstance();System.out.println(s5 == s6);System.out.println(s6 == s7);}).start();;}}
打印的结果:
main:true
main:true
true
true
分析:
DateUtils类:提供一个getInstance()方法,方法内部先进行判断,先从ThreadLocal实例中(static修饰)调用get()方法,ThreadLocal里面的泛型是SimplateDateFormat,get方法会看当前线程里面是否有SimpleDateFormat对象,如果有的话就直接返回,如果没有的话,就创建这个对象,然后调用set方法,保存在ThreadLocal中的ThreadLocalMap中,这个Map集合的key是当前这个线程对象,值,是这个线程中需要保存的数据对象s1,s2,s3都是在主线程中的,通过DateUtils.getInstance()方法,拿到SimpleDateFormat实例,处于同一线程,所以这这三个变量指向的是同一个对象s5,s6,s7处在工作线程中,ThreadLocal也会为这个工作线程保存一份SimpleDateFormat对象,所以这三个变量指向的是同一个对象
注意:主线程中的SimpleDateFormat对象跟工作线程中的SimpleDateFormat对象是不一样的.在验证的时候,有一个问题,
下面这种验证的方式是不合理的验证方法:
class DateUtils {private static ThreadLocal<SimpleDateFormat> td = new ThreadLocal<>();public static SimpleDateFormat getInstance() {SimpleDateFormat sdf = td.get();if (sdf == null) {sdf = new SimpleDateFormat("yyyy-MM-hh");}td.set(sdf);return sdf;}}public class ThreadLocalDemo2 {public static void main(String[] args) {SimpleDateFormat s1 = DateUtils.getInstance();SimpleDateFormat s2 = DateUtils.getInstance();SimpleDateFormat s3 = DateUtils.getInstance();// 打印s3System.out.println("s3" + s3);System.out.println("main:" + (s1 == s2));System.out.println("main:" + (s2 == s3));new Thread(() -> {SimpleDateFormat s5 = DateUtils.getInstance();SimpleDateFormat s6 = DateUtils.getInstance();SimpleDateFormat s7 = DateUtils.getInstance();//打印s5System.out.println("s5:" + s5);System.out.println(s5 == s6);System.out.println(s6 == s7);}).start();;}}
这个例子中输出主线程的s3的toString()方法,工作线程输出s5的toString()方法,进行比较,结果:
s3java.text.SimpleDateFormat@f67a0280main:truemain:trues5:java.text.SimpleDateFormat@f67a0280truetrue
想用toSting()方法来进行比较,是不可以的,以为toString()是利用HashCode生成字符串,即使是不同的对象也有可能HashCode是一样的,所以用toString()来比较对象是否是同一个是不正确的做法;
正确的做法:
class DateUtils {private static ThreadLocal<SimpleDateFormat> td = new ThreadLocal<>();public static SimpleDateFormat getInstance() {SimpleDateFormat sdf = td.get();if (sdf == null) {sdf = new SimpleDateFormat("yyyy-MM-hh");}td.set(sdf);return sdf;}}public class ThreadLocalDemo2 {//static SimpleDateFormat s5;public static void main(String[] args) {SimpleDateFormat s1 = DateUtils.getInstance();SimpleDateFormat s2 = DateUtils.getInstance();SimpleDateFormat s3 = DateUtils.getInstance();System.out.println("main:" + (s1 == s2));System.out.println("main:" + (s2 == s3));new Thread(() -> {SimpleDateFormat s5 = DateUtils.getInstance();SimpleDateFormat s6 = DateUtils.getInstance();SimpleDateFormat s7 = DateUtils.getInstance();System.out.println(s3 == s5);System.out.println(s5 == s6);System.out.println(s6 == s7);}).start();}}
这种比较方式是合理的,只能采用 == 的方式去比较, == 是比较两个对象是否是同一个对象
使用ThreadLocal还有一种方法:
class DateFormatUtils { private static ThreadLocal<SimpleDateFormat> td = new ThreadLocal<SimpleDateFormat>(){protected SimpleDateFormat initialValue() {System.out.println("=================initialValue()=================");return new SimpleDateFormat("yyyy-MM-hh");};};public static String convertDate(Date date) {return td.get().format(date);}}public class ThreadLocalDemo3 {public static void main(String[] args) {String dateStr1 = DateFormatUtils.convertDate(new Date());String dateStr2 = DateFormatUtils.convertDate(new Date());String dateStr3 = DateFormatUtils.convertDate(new Date());new Thread(()->{String dateStr4 = DateFormatUtils.convertDate(new Date());String dateStr5 = DateFormatUtils.convertDate(new Date());String dateStr6 = DateFormatUtils.convertDate(new Date());}).start(); }}
这是用Threal的inintalValue()方法,每一次有别的对象要调用DateFormatUtils的converDate()方法时,就会创建SimpleDateFormat对象,为为当前线程创建一个对象,之后就不会为这个线程创建了,都是使用同一个,
注意initialValue()是在匿名的ThreadLocal的子类里面重写父类的方法
上面这个例子,开启了两个线程,那么就会走两次initialValue()方法,创建两次SimpleDateFormat对象
打印结果:
=================initialValue()=================
=================initialValue()=================
ThreadLocal的详细解释(具体例子)
案例一:
public class ThreadLocalTest {
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {//重写父类的方法protected Integer initialValue() {return 0;};
};
//计数
static class Counter implements Runnable {public void run() {//获取当前线程的变量,然后累加100次int num = local.get();for (int i = 0; i < 100; i++) {num++;}//重新设置累加后的本地变量local.set(num);System.out.println(Thread.currentThread().getName() + " : " + local.get());}
}public static void main(String[] args) {Thread[] threads = new Thread[5];for (int i = 0; i < 5; i++) {threads[i] = new Thread(new Counter(),"CounterThread-[" + i +"]");threads[i].start();}}
}
这个例子说明了ThreadLocal没每一个线程单独保存一份副本:
打印结果:(五个线程打印的变量都是一致的)
CounterThread-[0] : 100CounterThread-[1] : 100 CounterThread-[3] : 100CounterThread-[2] : 100CounterThread-[4] : 100
案例二:(下面这个例子会出现问题)
package threadLocal;public class ThreadLocalTest01 {static class Index {private int num;public void increase() {num++;}public int getValue() {return num;}}private static Index num = new Index();//创建一个Index型的线程本地变量public static final ThreadLocal<Index> local = new ThreadLocal<Index>() {protected Index initialValue() {System.out.println(num.getValue());return num;}};//计数static class Counter implements Runnable {public void run() {Index num = local.get();for (int i = 1; i < 1000; i++) {num.increase();}//重新设置累加后的本地变量local.set(num);System.out.println(Thread.currentThread().getName() + " : " + local.get().getValue());}}public static void main(String[] args) {Thread[] threads = new Thread[5];for (int i = 0; i < 5; i++) {threads[i] = new Thread(new Counter(),"CounterThread-[" + i + "]");}for (int i = 0; i < 5; i++) {threads[i].start();}}}
0000CounterThread-[3] : 2997CounterThread-[2] : 39960CounterThread-[0] : 4995CounterThread-[4] : 1998CounterThread-[1] : 999
这个案例出现问题的原因:ThreadLocal的initialValue()方法,返回的是num(引用),这是有问题的,相当于ThreadLocal每一次是把一个引用当做副本保存在作为当前线程的值来保存,这样会出问题的,因为这个引用是会改变值的,只需要把return num; 换成 return new Index(),这样就不会有问题.
案例三(内存泄露与WeakReference)
public class ThreadLocalTest02 { public static class MyThreadLocal extends ThreadLocal {private byte[] a = new byte[1024 * 1024 * 1];@Overridepublic void finalize() {System.out.println("My threadlocal 1 MB finalized.");}}public static class My50MB {// 占用内存的大对象private byte[] a = new byte[1024 * 1024 * 50];@Overridepublic void finalize() {System.out.println("My 50 MB finalized.");}}public static void main(String[] args) throws InterruptedException {new Thread(new Runnable() {@Overridepublic void run() {ThreadLocal tl = new MyThreadLocal();tl.set(new My50MB());tl = null;// 断开ThreadLocal的强引用System.out.println("Full GC 1");System.gc();}}).start();System.out.println("Full GC 2");System.gc();Thread.sleep(1000);System.out.println("Full GC 3");System.gc();Thread.sleep(1000);System.out.println("Full GC 4");System.gc();Thread.sleep(1000);}}
打印结果:Full GC 2Full GC 1My threadlocal 1 MB finalized.Full GC 3My 50 MB finalized.Full GC 4
分析:从输出可以看出,一旦threadLocal的强引用断开,key的内存就可以得到释放。只有当线程结束后,value的内存才释放。
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap。Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指threadlocal实例,所以threadlocal将会被gc回收。但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。
只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
ThrealLocal的内存泄漏问题:
看一个案例:
public class ThreadLocalTest02 { public static class MyThreadLocal extends ThreadLocal {private byte[] a = new byte[1024 * 1024 * 1];@Overridepublic void finalize() {System.out.println("My threadlocal 1 MB finalized.");}}public static class My50MB {// 占用内存的大对象private byte[] a = new byte[1024 * 1024 * 50];@Overridepublic void finalize() {System.out.println("My 50 MB finalized.");}}public static void main(String[] args) throws InterruptedException {new Thread(new Runnable() {@Overridepublic void run() {ThreadLocal tl = new MyThreadLocal();tl.set(new My50MB());tl = null;// 断开ThreadLocal的强引用System.out.println("Full GC 1");System.gc();}}).start();System.out.println("Full GC 2");Thread.sleep(3000);System.gc();System.out.println("..........");System.out.println("..........");System.out.println("..........");System.out.println("..........");System.out.println("..........");System.out.println("..........");}}
看看打印结果来分析:
Full GC 2Full GC 1My threadlocal 1 MB finalized.............................................................My 50 MB finalized.
这里面有两个类.一个是MyThreadLocal继承了ThreadLocal,里面有一个成员变量(用来模拟占用比较的的内存),一个字节写的一个类,MyThreadLocal的key是当前的线程对象,value是My50MB这个类对象,main方法里面,将t1 = null,断开了ThreadLocal的强引用,然后强烈建议gc过来回收,MyThreadLocal会立即回收,但是My50MB这个类的对象并不会立即回收,有时需要等程序运行结束,线程结束之后,才会回收
解释:
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap。Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用
只是针对key。每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指threadlocal实例,所以threadlocal将会被gc
回收。但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。
只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露。但是value在threadLocal设为null和线程结束这段时间不会被回收,就发生了我们认为
的“内存泄露”。使用ThreadLocal需要注意,每次执行完毕后,要使用remove()方法来清空对象,否则 ThreadLocal 存放大对象后,可能会OMM。
为什么使用弱引用:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
关于弱引用,老师也讲了这两个案例:
案例一:class Outer01 {/*** 内存泄漏就是对象没有强引用只用了,但是垃圾回收机制没有回收* 内存泄漏是造成内存溢出的原因*/class Inner01 extends Thread {public void run() {while(true) {}}}public Outer01() {new Inner01().start();}@Overrideprotected void finalize() throws Throwable {System.out.println("finalize()");}}public class ThreadLocalDemo4 {public static void main(String[] args) {Outer01 o = new Outer01();o = null;System.gc();}}
这会造成内存泄漏问题,即使Outer01的对象设置为空了,并且强烈建议gc来回收,但是,由于如果内部类没有加上static修饰的内部类,
那么这个内部类是要依存于外部类(内部类没有停止的时候,外部类也不能被回收)
该进的方法:就是给外部类加上static修饰,
内部类加上了static修饰之后,那么这个内部类就不需要依赖外部类,即使内部类还在运行,外部类没有强引用,gc可以进来会说这个外部类.
案例二:
强引用:class TQueue {private Outer02 outer02;public TQueue(Outer02 outer02) {this.outer02 = outer02;}@Overrideprotected void finalize() throws Throwable {System.out.println("TQueue.finalize()");}}class Outer02 {public Outer02() {new Inner02(new TQueue(this)).start();;}static class Inner02 extends Thread {private TQueue tQueue;public Inner02(TQueue tQueue) {this.tQueue = tQueue;}public void run() {while(true) {System.out.println(tQueue);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}@Overrideprotected void finalize() throws Throwable {System.out.println("Outer02.finalize()");}}public class ThreadDemo5 {public static void main(String[] args) {Outer02 o = new Outer02();o=null;System.gc();//while(true){}}}
分析:
这里面是存在强引用的,new Outer02()创建这个对象的时候,需要创建Innerer02(线程类的的对象),并且启用这个线程,这个线程类有需要创建TQueue这个对象,
而TQueue对象由得依赖Outer02这个对象,
他们直接是强引用的关系
Outer02 —> Inter02 –> TQueue –> Outer02 (存在着强引用)
打印结果:
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
Outer02这个对象并没有被回收,这就存在内存泄漏问题,因为Outer02这个类的对象已经置为空了,并且强烈建议gc来回收但是仍然没有,
即使Outer02的内部类Inner02这个类用static修饰,也没有用,以为Inteer02需要引用TQueue,而TQueue又需要Outer02,并且static是只能够加在内部类上的,外部类是不能加static修饰的.
解决这个问题的办法就是得用弱引用.
package threadLocal;import java.lang.ref.WeakReference;class TQueue {private Outer02 outer;public TQueue(Outer02 outer) {this.outer = outer;}@Overrideprotected void finalize() throws Throwable {System.out.println("TQueue.finalize()");}}class Outer02 {public Outer02() {new Inner02(new TQueue(this)).start();}static class Inner02 extends Thread {/** private TQueue tQueue; public Inner02(TQueue tQueue){* this.tQueue=tQueue; }*/// 弱引用private WeakReference<TQueue> weakR;public Inner02(TQueue tQueue) {this.weakR = new WeakReference<TQueue>(tQueue);}@Overridepublic void run() {while (true) {// 获取弱引用引用的对象System.out.println(this.weakR.get());try {Thread.sleep(1000);} catch (Exception e) {}}}}@Overrideprotected void finalize() throws Throwable {System.out.println("finalize()");}}public class TestOOM02 {public static void main(String[] args) {// 强引用Outer02 o2 = new Outer02();o2 = null;System.gc();// while(true){}}}
这里面试存在弱引用的,所以是可以被gc回收的.