1.理解ThreadLocal
*1.1
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出
优美的多线程程序。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它
线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
*1.2
ThreadLocal类接口很简单,只有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.3
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?
其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本
2.案例实现
需求:SimpleDateFormat是线程不安全的类,但是在项目中我们需要这个类来进行日期的转换,如果有一个页面需要进行容器格式的转换,多个
人共同访问的话,默认用的是同一个SimpleDateFormat的话(写在容器转换类里面,用static修饰),会容易出现线程安全问题,如果每一个,如果这个SimpleDateFormat不进行共享的话,每次访问都创建对象的话,那么会创建太多的对象,内存消耗比较大
这时就有比较折中的方法,就是为每一个线程单独创建一个SimpleDateFormat,这样在同一个线程内部,用的是同一个SimpleDateFormat,但是在不同的线程,使用的SimpleDateFormat是不一样的
看代码实现:
class DateUtils {
/*
* SimpleDateFormet 1)线层不安全 2)此对象不能被多个线程共享(局部性能不好)
* 3)可以将此独享设置为线程内部单例(内个线程有一份)
*/
/*
* ThreadLocal对象提供了这样一种机制: 1)可以将某个对象绑定到当前线程 2)可以从某个线程获取某个对象
*/
private static ThreadLocal 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 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();
// 打印s3
System.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();
//打印s5
System.out.println("s5:" + s5);
System.out.println(s5 == s6);
System.out.println(s6 == s7);
}).start();
;
}
}
这个例子中输出主线程的s3的toString()方法,工作线程输出s5的toString()方法,进行比较,结果:
s3java.text.SimpleDateFormat@f67a0280
main:true
main:true
s5:java.text.SimpleDateFormat@f67a0280
true
true
想用toSting()方法来进行比较,是不可以的,以为toString()是利用HashCode生成字符串,即使是不同的对象也有可能HashCode是一样的,所以用toString()来比较对象是否是同一个是不正确的做法;
正确的做法:
class DateUtils {
private static ThreadLocal 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 td = new ThreadLocal(){
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()=================
案例一:
public class ThreadLocalTest {
public static final ThreadLocal local = new ThreadLocal() {
//重写父类的方法
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] : 100
CounterThread-[1] : 100
CounterThread-[3] : 100
CounterThread-[2] : 100
CounterThread-[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 local = new ThreadLocal() {
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();
}
}
}
0
0
0
0
CounterThread-[3] : 2997
CounterThread-[2] : 3996
0
CounterThread-[0] : 4995
CounterThread-[4] : 1998
CounterThread-[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];
@Override
public void finalize() {
System.out.println("My threadlocal 1 MB finalized.");
}
}
public static class My50MB {// 占用内存的大对象
private byte[] a = new byte[1024 * 1024 * 50];
@Override
public void finalize() {
System.out.println("My 50 MB finalized.");
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public 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 2
Full GC 1
My threadlocal 1 MB finalized.
Full GC 3
My 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回收.
看一个案例:
public class ThreadLocalTest02 {
public static class MyThreadLocal extends ThreadLocal {
private byte[] a = new byte[1024 * 1024 * 1];
@Override
public void finalize() {
System.out.println("My threadlocal 1 MB finalized.");
}
}
public static class My50MB {// 占用内存的大对象
private byte[] a = new byte[1024 * 1024 * 50];
@Override
public void finalize() {
System.out.println("My 50 MB finalized.");
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public 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 2
Full GC 1
My 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();
}
@Override
protected 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;
}
@Override
protected 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();
}
}
}
}
@Override
protected 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;
}
@Override
protected 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 weakR;
public Inner02(TQueue tQueue) {
this.weakR = new WeakReference(tQueue);
}
@Override
public void run() {
while (true) {
// 获取弱引用引用的对象
System.out.println(this.weakR.get());
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
}
}
@Override
protected 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回收的.