背景
假如有个需求,两个方法不通过参数传递,能不能实现?这么说还不够具体,直接上程序说明。
public class TestClass {
public void methodA() {
String var = "methodA";
methodB();
}
public void methodB() {
System.out.println(var);
}
}
假设我有一个测试类TestClass,它里面有两个方法methodA和methodB,这两个方法都没有定义形参。methodA通过计算得到一个结果var,那么我如何在methodB方法中获取到这个var呢?很容易想到,我直接在TestClass类中定义一个成员变量不就能解决了?
版本1
public class TestClass {
private String var;
public void methodA() {
var = "methodA";
methodB();
}
public void methodB() {
System.out.println(var);
}
}
嗯,这个确实可以解决methodB方法访问var的问题,但是很明显存在线程安全问题。另外,如果换种方式,此种解法也未必适用。假设methodA和methodB分别位于不同的类。
版本2
Test1Class如下:
public class Test1Class {
public void methodA() {
String var = "methodA";
Test2Class instance = new Test2Class();
instance.methodB();
}
}
Test2Class如下:
public class Test2Class {
public void methodB() {
System.out.println(var);
}
}
这种情况,就算你在Test1Class中定义成员变量var,那么你在Test2Class的methodB也无法访问到var,或许你会说在mehtodB上增加一个形参不就可以了吗?但是我们的前提是methodA和methodB都没有入参。于是,你又想到了解决方法,定义类静态变量。
版本3
Test1Class如下:
public class Test1Class {
static String var;
public void methodA() {
var = "methodA";
Test2Class instance = new Test2Class();
instance.methodB();
}
}
Test2Class如下:
public class Test2Class {
public void methodB() {
System.out.println(Test1Class.var);
}
}
这样虽然可以解决访问var的问题,但是是不是又回到方案1上去了,依然会存在线程安全问题。所以,解决问题的关键落在了,你要将变量var绑定到执行线程上,然后在methodB根据当前线程取出绑定的变量var即可。没错,JDK已经给我们提供了实现,就是ThreadLocal。
最佳的版本
public class Test1Class {
static ThreadLocal<String> threadLocals = new ThreadLocal<>();
public void methodA() {
String var = "methodA";
threadLocals.set(var);
Test2Class instance = new Test2Class();
instance.methodB();
}
}
当然,一般的情况,这个threadLocals是定义在其他的地方,不一定要定义在Test1Class中。
public class Test2Class {
public void methodB() {
String var = Test1Class.threadLocals.get();
System.out.println(var);
}
}
这样我们就能够不通过方法参数传递,也能在methodB中获取methodA中设定的值,并且能够保证线程安全。
二、神奇的弱引用
1、我们先定义一个简单的类,假设为A。A内部持有一个ThreadLocal类型的成员变量local。
public class A {
private ThreadLocal<String> local = new ThreadLocal<>();
// 在构造A类实例时,设置一个值
public A() {
local.set("AClass");
}
public String get() {
return local.get();
}
public void set(String s) {
local.set(s);
}
}
我们知道ThreadLocal是通过操作线程Thread内部维护的一个ThreadLocalMap来实现将变量绑定到线程上的,并且Map中的Entry的Key持有local的弱引用的。弱引用的存在,又导致垃圾回收线程进行垃圾回收时,不管当前内存空间是否足够,都会回收掉弱引用对象。
2、接下来要验证是否真的是这样。
public class ThreadLocalLecture {
public static void main(String[] args) {
ThreadLocalLecture lecture = new ThreadLocalLecture();
lecture.holdObject();
}
private void holdObject() {
A instance = new A();
String val = instance.get();
System.out.println("val: " + val);
Thread t = Thread.currentThread();
System.gc();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName());
}
}
执行A instance = new A();之后,instance就是一个强引用,并且A.local也是强引用。
借此机会,我们看一下Thread实例t内部的threadlocals。
threadLocals内部也是一个数组,目前共有4个Entry,有一个value为AClass是我们关注的,对应的reference就是Entry的Key。
继续执行System.gc()显式进行垃圾回收,回收之后,却发现,这个reference并没有被回收。
3、注意看,接下来的事情会更有意义。
public class ThreadLocalLecture {
public static void main(String[] args) {
ThreadLocalLecture lecture = new ThreadLocalLecture();
lecture.holdObject();
}
private void notHold() {
new A();
Thread t = Thread.currentThread();
System.gc();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName());
}
}
首先,直接执行new A();然后这个实例,没有定义一个强引用,也就是说没有办法从GC ROOT发现这个对象,那我们理解为其实它是一个弱引用。我们也观察一下此时Thread实例t内部的threadlocals。
与上面2中的一模一样。接下来,继续显式执行垃圾回收。执行之后,继续查看t内部的threadlocals。
结果却发现,value为AClass的那个Entry的Key确实是被回收掉了。仔细想想,A实例对象无法通过GC ROOT发现,那么A内部的local更加不能被发现,所以它是一个弱引用。由于t内部的threadlocals的Entry的Key是持有local的弱引用的,这种情况下,GC回收器是可以回收掉Key的。
这样是不是存在问题,Key被回收掉了,value依然存在,但是又无法通过Key来获取到value,所以这个value的存在是没有意义的,即发生了所谓的内存泄漏。
4、结论
a、ThreadLocalMap的Entry继承了WeakReference,并不是说这个Entry是弱引用,WeakReference只是一个容器,它内部的reference才是真正的弱引用,也就是Entry的Key。
b、ThreadLocal虽然使用了弱引用,但是有造成内存泄漏的风险,所以编码时要注意。