背景
假如有个需求,两个方法不通过参数传递,能不能实现?这么说还不够具体,直接上程序说明。
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虽然使用了弱引用,但是有造成内存泄漏的风险,所以编码时要注意。
