背景

假如有个需求,两个方法不通过参数传递,能不能实现?这么说还不够具体,直接上程序说明。

  1. public class TestClass {
  2. public void methodA() {
  3. String var = "methodA";
  4. methodB();
  5. }
  6. public void methodB() {
  7. System.out.println(var);
  8. }
  9. }

假设我有一个测试类TestClass,它里面有两个方法methodA和methodB,这两个方法都没有定义形参。methodA通过计算得到一个结果var,那么我如何在methodB方法中获取到这个var呢?很容易想到,我直接在TestClass类中定义一个成员变量不就能解决了?

版本1

  1. public class TestClass {
  2. private String var;
  3. public void methodA() {
  4. var = "methodA";
  5. methodB();
  6. }
  7. public void methodB() {
  8. System.out.println(var);
  9. }
  10. }

嗯,这个确实可以解决methodB方法访问var的问题,但是很明显存在线程安全问题。另外,如果换种方式,此种解法也未必适用。假设methodA和methodB分别位于不同的类。

版本2

Test1Class如下:

  1. public class Test1Class {
  2. public void methodA() {
  3. String var = "methodA";
  4. Test2Class instance = new Test2Class();
  5. instance.methodB();
  6. }
  7. }

Test2Class如下:

  1. public class Test2Class {
  2. public void methodB() {
  3. System.out.println(var);
  4. }
  5. }

这种情况,就算你在Test1Class中定义成员变量var,那么你在Test2Class的methodB也无法访问到var,或许你会说在mehtodB上增加一个形参不就可以了吗?但是我们的前提是methodA和methodB都没有入参。于是,你又想到了解决方法,定义类静态变量。

版本3

Test1Class如下:

  1. public class Test1Class {
  2. static String var;
  3. public void methodA() {
  4. var = "methodA";
  5. Test2Class instance = new Test2Class();
  6. instance.methodB();
  7. }
  8. }

Test2Class如下:

  1. public class Test2Class {
  2. public void methodB() {
  3. System.out.println(Test1Class.var);
  4. }
  5. }

这样虽然可以解决访问var的问题,但是是不是又回到方案1上去了,依然会存在线程安全问题。所以,解决问题的关键落在了,你要将变量var绑定到执行线程上,然后在methodB根据当前线程取出绑定的变量var即可。没错,JDK已经给我们提供了实现,就是ThreadLocal。

最佳的版本

  1. public class Test1Class {
  2. static ThreadLocal<String> threadLocals = new ThreadLocal<>();
  3. public void methodA() {
  4. String var = "methodA";
  5. threadLocals.set(var);
  6. Test2Class instance = new Test2Class();
  7. instance.methodB();
  8. }
  9. }

当然,一般的情况,这个threadLocals是定义在其他的地方,不一定要定义在Test1Class中。

  1. public class Test2Class {
  2. public void methodB() {
  3. String var = Test1Class.threadLocals.get();
  4. System.out.println(var);
  5. }
  6. }

这样我们就能够不通过方法参数传递,也能在methodB中获取methodA中设定的值,并且能够保证线程安全。


二、神奇的弱引用
1、我们先定义一个简单的类,假设为A。A内部持有一个ThreadLocal类型的成员变量local。

  1. public class A {
  2. private ThreadLocal<String> local = new ThreadLocal<>();
  3. // 在构造A类实例时,设置一个值
  4. public A() {
  5. local.set("AClass");
  6. }
  7. public String get() {
  8. return local.get();
  9. }
  10. public void set(String s) {
  11. local.set(s);
  12. }
  13. }

我们知道ThreadLocal是通过操作线程Thread内部维护的一个ThreadLocalMap来实现将变量绑定到线程上的,并且Map中的Entry的Key持有local的弱引用的。弱引用的存在,又导致垃圾回收线程进行垃圾回收时,不管当前内存空间是否足够,都会回收掉弱引用对象。

2、接下来要验证是否真的是这样。

  1. public class ThreadLocalLecture {
  2. public static void main(String[] args) {
  3. ThreadLocalLecture lecture = new ThreadLocalLecture();
  4. lecture.holdObject();
  5. }
  6. private void holdObject() {
  7. A instance = new A();
  8. String val = instance.get();
  9. System.out.println("val: " + val);
  10. Thread t = Thread.currentThread();
  11. System.gc();
  12. try {
  13. TimeUnit.SECONDS.sleep(1);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(t.getName());
  18. }
  19. }

执行A instance = new A();之后,instance就是一个强引用,并且A.local也是强引用。
ThreadLocal_in_A.jpg
借此机会,我们看一下Thread实例t内部的threadlocals。
A_hold_object.jpg
threadLocals内部也是一个数组,目前共有4个Entry,有一个value为AClass是我们关注的,对应的reference就是Entry的Key。
继续执行System.gc()显式进行垃圾回收,回收之后,却发现,这个reference并没有被回收。
do_not_collect_entry_key.jpg
3、注意看,接下来的事情会更有意义。

  1. public class ThreadLocalLecture {
  2. public static void main(String[] args) {
  3. ThreadLocalLecture lecture = new ThreadLocalLecture();
  4. lecture.holdObject();
  5. }
  6. private void notHold() {
  7. new A();
  8. Thread t = Thread.currentThread();
  9. System.gc();
  10. try {
  11. TimeUnit.SECONDS.sleep(1);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. System.out.println(t.getName());
  16. }
  17. }

首先,直接执行new A();然后这个实例,没有定义一个强引用,也就是说没有办法从GC ROOT发现这个对象,那我们理解为其实它是一个弱引用。我们也观察一下此时Thread实例t内部的threadlocals。
A_hold_weak_reference.jpg
与上面2中的一模一样。接下来,继续显式执行垃圾回收。执行之后,继续查看t内部的threadlocals。
do_collect_entry_key.jpg
结果却发现,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虽然使用了弱引用,但是有造成内存泄漏的风险,所以编码时要注意。