文章目录
前言
“水能载舟,亦能覆舟。” 用这句话来形容ThreadLocal 最贴切不过。ThreadLocal 初衷是在线程并发时,解决变量共享问题,但由于过度设计,比如弱引用和哈希碰撞,导致理解难度大、使用成本高,反而成为故障高发点,容易出现内存泄露、脏数据、共享对象更新等问题。单从ThreadLocal 的命名看人们会认为只要用它对了,包治变量共享问题,然而并不是。下面我们以内存模型、弱引用、哈希算法为铺垫,然后从CS 真人游戏的代码示例入手,详细分析ThreadLocal 源码。我们从中可以学习到全新的编程思维方式,并认识到问题的来源,也能够帮助我们谙熟此类的设计之道,扬长避短。
一、引用类型
在内存布局和垃圾回收中,对象在堆上创建之后所持有的引用其实是一种变量类型,引用之间可以通过赋值构成一条引用链。从GC Roots 开始遍历,判断引用是否可达。引用的可达性是判断能否被垃圾回收的基本条件。JVM 会据此自动管理内存的分配与回收,不需要开发工程师干预。但在某些场景下,即使引用可达,也希望能够根据语义的强弱进行有选择的回收,以保证系统的正常运行。根据引用类型语义的强弱来决定垃圾回收的阶段,我们可以把引用分为强引用、软引用、弱引用和虚引用四类。后三类引用,本质上是可以让开发工程师通过代码方式来决定对象的垃圾回收时机。
-
强引用,即 Strong Reference
最为常见,如 Object obj = new Object(); 这样的变量声明和定义就会产生对该对象的强引用。只要有强引用指向,并且 GC Roots 可达,那么Java 内存回收时,即使濒临内存耗尽,也不会回收该对象。 -
软引用,即Soft Reference
引用力度弱于“强引用” ,是用在非必要对象的场景。在即将OOM 之前,垃圾回收会把这些软引用指向的对象加入回收范围,以获得更多的内存空间,让程序能够继续健康运行。主要用来缓存服务器中间计算结果及不需要实时保存的用户行为等。 -
弱引用,即Weak Reference
应用强度较前两者更弱,也是用来描述非必要对象的。如果弱引用指向的对象只存在弱引用这一条线路,则在下一次YGC 时会被回收。由于YGC 时间的不确定性,弱引用何时被回收也具有不确定性。弱引用主要用于指向某个易消失的对象,在强引用断开后,此引用不会劫持对象。调用WeakReference.get() 可能返回null,要注意空指针异常。 -
虚引用,即Phantom Reference
是极弱的一种引用关系,定义完成后,就无法通过该引用获取指向的对象。为一个对象设置虚引用的唯一目的就是希望能在这个对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,当垃圾回收时,如果发现存在虚引用,就会在回收对象内存前,把这个虚引用加入与之关联的引用队列中。
对象的引用类型如下图所示:
以上图举个例子,在房产交易市场中,某个卖家有一套房子,成功出售给某个买家后引用置为null。这里有4个买家使用4种不同的引用关系指向这套房子。买家buyer1 是强引用,如果把seller 引用赋值给它,则永久有效,系统不会因为 seller = null 就触发对这套房子的回收,这是房屋交易市场最常见的交付方式。买家buyer2 是软引用,只要不产生OOM,buyer2.get() 就可以获取房子对象,就像房子是租来的一样。买家buyer3 是弱引用,一旦过户后,seller 置为null,buyer3 的房子持有时间估计只有几秒钟,卖家只是给买家做了一张假的房产证,买家高兴了几秒钟后,发现房子已经不是自己的了。buyer4 是虚引用,定义完成后无法访问到房子对象,卖家只是虚构了房源,是空手套白狼的诈骗术。
1.1 软引用的回收机制
首先设置JVM 参数:-Xms20m -Xmx20m ,即使只有20MB的堆内存空间。在下方的示例代码中不断地往集合里添加House 对象,而每个House 有2000 个Door 成员变量,狭小的堆空间加上大对象的产生,就是为了尽快触达内存耗尽的临界状态:
public class SoftreferenceHose {
public static void main(String[] args) {
// List<House> houses = new ArrayList<>(); (第1处)
List<SoftReference> houses = new ArrayList<>();
// 剧情反转注释处
int i = 0;
while (true){
// houses.add(new House()); (第2处)
// 剧情反转注释处
SoftReference<House> buyer2
= new SoftReference<House>(new House());
// 剧情反转注释处
houses.add(buyer2);
System.out.println("i="+(++i));
}
}
}
class House{
private static final Integer DOOR_NAMBER = 2000;
public Door[] doors = new Door[DOOR_NAMBER];
class Door{
}
}
new House() 是匿名对象,产生之后即赋值给软引用。正常运行一段时间后,内存到达耗尽的临界点,House$Door 超过10MB左右,内存占比达到80.4%。
软引用的特性在数秒之后产生价值,House对象数从千数量级迅速降到百数量级,内存容量迅速被释放出来,保证了程序的正常运行。
软引用SoftReference 的父类 Reference 的属性: private T referent,它指向new House() 对象,而SoftReference 的get() ,也是调用了 super.get() 来访问父类这个私有属性。大量的House 在内存即将耗尽前,成功地一次又一次被清理掉。对象buyer2虽然是引用类型,但其本身还是占用一定内存空间的,它是被集合ArrayList 强引用劫持的。在不断循环执行house.add() 后,在i = 3600035时,终于产生了OOM。软引用、弱引用、虚引用均存在带有队列的构造方法:
pubic SoftReference(T referenceQueue<? super T>q){
...}
可以在队列中检查哪个软引用的对象被回收了,从而把失去House 的软引用对象清理掉。
反转一下剧情。在同一个类中,使用完全相同的运行环境和内存参数,把SoftReference 中被注释掉的两句代码激活(即示例代码中的第1处和第2处),同时把在后边标记了“剧情反转注释处” 的3局代码注释掉,再次运行。观察一下,在没有软引用的情况下,这个循环能够撑多久?运行得到的结果在i = 2404 时,就产生OOM 异常。这个示例简单地证明了软引用在内存紧张情况下的回收能力。软引用一般用于在同一服务器内缓存中间结果。如果命中缓存,则提取缓存结果,否则重新计算或获取。但是,软引用肯定不是用来缓存高频数据的,万一服务器重启或者软引用触发大规模回收,所有的访问将直接指向数据库,导致数据库的压力时大时小,甚至崩溃。
如果内存没有达到OOM,软引用持有的对象会被回收吗?下面代码来验证下:
public class SoftReferenceWhenIdle {
public static void main(String[] args) {
House seller = new House();
// (第1处)
SoftReference<House> buyer2 = new SoftReference<>(seller);
seller = null;
while (true){
// 下方两句代码建议JVM 进行垃圾回收
System.gc();
System.runFinalization();
if(buyer2.get() == null){
System.out.println("house is null");
break;
}else {
System.out.println("still there");
}
}
}
}
System.gc() 方法建议垃圾收集器尽快进行垃圾收集,具体何时执行仍由JVM 来判断。System.runFinalization() 方法的作用是强制调用已经失去引用对象的finalize() 。
在代码中同时调用这两者,有利于更快地执行垃圾回收。在相同的运行环境下,一直输出still there ,说明buyer2 一直持有new House() 的有效引用。如果对方置为null 时仍能自动感知,并且主动断开引用指向的对象,这是哪种引用方式可以担负的使用? 答案是弱引用。事实上,把示例代码中第1 处的两个红色SoftReference 修改为WeakReference 即可实现回收。
1.2 弱引用的回收机制
出于对WeakReference 的尊重,摒弃刚才催促垃圾回收的代码,让WeakReference 自然地被YGC 回收,使对象能够存活更长的时间。我们可以在JVM 启动参数加 -XX:+printGCDetails (或高版本JDK使用 -Xlog:gc)来观察GC 的触发情况:
public class WeakReferenceWhenIdle {
public static void main(String[] args) {
House seller = new House();
WeakReference<House> buyer3 = new WeakReference<>(seller);
seller = null;
long start = System.nanoTime();
int count = 0;
while(