写在前面:
文中涉及到的代码均出自jdk1.8
正文
强引用、软引用、弱引用和虚引用的概念我就不说了,网上一搜一大把,强应用和软引用的应用场景也是不少,但是这个弱引用,就没见有人讲明白过,今天,我来试试。
今天呢,我得从ThreadLocal讲起。
ThreadLocal是什么?
ThreadLocal是线程本地变量,通俗点说就是如果一个变量是ThreadLocal类型的,那么每个线程都会创建这个变量的副本,并各自维护。
这听起来有些像局部变量,是的,你可以将它理解为作用范围是全局的局部变量。(听起来真绕口,但是我觉的这样说你会懂的)
它最经典的应用场景就是数据库的Connection,它可以保证同一个线程的多个方法使用同一个Connection,从而实现跨方法的事务控制.
下面来看一个ThreadLocal的例子
我定义了一个全局变量tl,并通过两个线程去循环递增7次,我们可以看到最后的结果是14,因为tl是两个线程共有的,所以是两个线程累积递增的结果.(我们暂时先不考虑多线程造成的影响)
/**
* @author: cuijr
* @email:
* @date: 2021-01-26 19:37
* @description:
*/
public class ThreadLocalTest {
/**
* 定义int变量,并初始化为0
*/
private int tl = 0;
/**
* 获取变量tl,并递增至7
*/
private void add() {
for (int i = 0; i < 7; i++) {
tl++;
System.out.println(Thread.currentThread().getName() + " current number is " + tl);
}
}
public static void main(String[] args) {
ThreadLocalTest tlt = new ThreadLocalTest();
new Thread(() -> tlt.add()).start();
new Thread(() -> tlt.add()).start();
}
}
// output
Thread-0 current number is 1
Thread-0 current number is 2
Thread-0 current number is 3
Thread-0 current number is 4
Thread-0 current number is 5
Thread-0 current number is 6
Thread-0 current number is 7
Thread-1 current number is 8
Thread-1 current number is 9
Thread-1 current number is 10
Thread-1 current number is 11
Thread-1 current number is 12
Thread-1 current number is 13
Thread-1 current number is 14
那如果我们把tl变量改成ThreadLocal类型,结果会有什么不同呢?
/**
* @author: cuijr
* @email:
* @date: 2021-01-26 19:37
* @description:
*/
public class ThreadLocalTest {
/**
* 定义ThreadLocal变量,并初始化为0
*/
private ThreadLocal<Integer> tl = ThreadLocal.withInitial(() -> 0);
/**
* 获取变量tl,并递增至7
*/
private void add() {
for (int i = 0; i < 7; i++) {
if (i == 5) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Integer a = tl.get() + 1;
tl.set(a);
System.out.println(Thread.currentThread().getName() + " current number is " + tl.get());
}
}
public static void main(String[] args) {
ThreadLocalTest tlt = new ThreadLocalTest();
new Thread(() -> tlt.add()).start();
new Thread(() -> tlt.add()).start();
}
}
//output
Thread-0 current number is 1
Thread-0 current number is 2
Thread-0 current number is 3
Thread-0 current number is 4
Thread-0 current number is 5
Thread-1 current number is 1
Thread-1 current number is 2
Thread-1 current number is 3
Thread-1 current number is 4
Thread-1 current number is 5
Thread-0 current number is 6
Thread-1 current number is 6
Thread-1 current number is 7
Thread-0 current number is 7
我们可以清楚地看到,Thread-0和Thread-1的tl变量分别递增到了7,可以证明这两个线程各自拥有一个tl的实例。
那ThreadLocal是怎么实现在每个线程中各保存一个变量的副本呢?
普通类中,如果我们想让类的每个实例都拥有一个变量的副本,那么方法很简单,就是定义一个成员变量,(也叫全局变量)那么每个对象创建的时候都会各自保存这个成员变量的副本。
同样的,线程也可以这么干!
每个线程都会有各自的存储空间,如果我们在Thread类里定义一个成员变量,那么每个线程不就会各自保存这个成员变量的副本了吗?是的!是的!!是的!!!Thread类里有个成员变量叫threadLocals,它的类型是ThreadLocalMap,这个成员变量里存储的就是ThreadLocal,而这,也是ThreadLocal为什么能在不同的线程中拥有不同副本的根本原因。
那为什么threadLocals对ThreadLocal的引用是弱引用呢?
通过上面的实例,我们知道了我们自己的程序中会定义ThreadLocal变量,并使用强引用引用他们,当我们定义的ThreadLocal变量不再被需要时,这个变量应该会被gc回收掉,否则就会造成内存泄漏。但现在除了我们自己程序里会强应用ThreadLocal变量外,线程对象里的threadLocals也会引用这个ThreadLocal变量,如果这个引用是强引用的话,那么它就无法会被回收,直到当前线程结束。如果当前线程是在线程池里,那。。。但是,如果这个引用是弱引用呢?当我们使用完ThreadLocal变量之后,自己程序里对ThreadLocal变量的强引用就没有了,只剩下当前线程中threadLocals对它的弱引用,那么下次垃圾回收时就会把ThreadLocal变量回收掉,就不会造成内存泄漏了。
ThreadLocal里为什么可以使用弱引用?
我觉得很重要的一点是:被弱引用的对象必须要被一个无法轻易操作的类引用。
怎么理解这句话呢?
还是以ThreadLocal为例,ThreadLocal对象本身必须被当前线程持有,这个当前线程时我们无法轻易操作的。我们用普通的强引用使用ThreadLocal变量,当不需要这个变量时,这个强引用就没了,只剩下个弱引用,那这个ThreadLocal变量自然就可以被回收了。
补充:
当然,当前线程对我们来说并不是不可操作的,毕竟我们可以通过Thread.currentThread().threadLocals来获取这个map,然后通过remove来移除不用的对象。
但是,如果每用完一个ThreadLocal变量,就需要调用Thread.currentThread().threadLocals去删除它,那么这个ThreadLocal也太难用了,这能是Doug lea这等大神容许的?所以在这里,弱引用是个极好的选择。