Java对synchronized锁的实现与优化以及四大引用

早期的synchronized锁

在Java 1.5之前,多线程并发中,synchronized一直都是一个元老级关键字,而且给人的一贯印象就是一个比较重的锁。为此,在Java 1.6之后,这个关键字被做了很多的优化,从而让以往的“重量级锁”变得不再那么重。

synchronized主要有两种使用方法,一种是修饰代码块,一种是修饰方法。这两种用法底层究竟是怎么实现的呢?在1.6之前是怎么实现的呢?

在java语言中存在两种内建的synchronized语法:

  1. synchronized语句;
  2. synchronized方法;

对于synchronized语句当Java源代码被javac编译成字节码的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit的字节码指令。

而synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在JVM字节码层面并没有任何特别的指令来标记被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示做为锁对象。

那么monitorenter和monitorexit以及access_flags底层又是通过什么技术来实现的原子操作呢?

互斥锁(Mutex Lock)

简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起(线程状态变为阻塞)并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的

Mutex Lock互斥锁主要用于实现内核中的互斥访问功能。Mutex Lock内核互斥锁是在原子API之上实现的,但这对于内核用户是不可见的。对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。使用中的互斥锁是不能被重新初始化的。并且互斥锁不能用于中断上下文。但是互斥锁比当前的内核信号量选项更快,并且更加紧凑,因此如果它们满足您的需求,那么它们将是您明智的选择。

在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制;OS基于这几个CPU硬件机制,就能够实现锁;再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等)。所以要想理解OS的各种同步手段,首先需要理解cpu层面的锁,这是最原点的机制,所有的OS上层同步手段都基于此。

Mutex Lock从用户态切换到内核态

Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

内核态:CPU可以访问内存所有数据,包括外围设备,例如硬盘、 网卡。CPU也可以将自己从一个程序切换到另一个程序。

用户态:只能受限的访问内存,且不允许访问外围设备。占用CPU的能力被剥夺,CPU资源可以被其他程序获取。

因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以为了减少有限资源的访问和使用冲突,Unix/Linux的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel的X86架构的CPU提供了0到3四个特权级,数字越小,特权越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换,类似的函数还有printf(),调用的是wirte()系统调用来输出字符串,等等

所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情,例如从硬盘读取数据,或者从键盘获取输入等。而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先请求操作系统以程序的名义来执行这些操作。

这时需要一个这样的机制:用户态程序切换到内核态,但是不能控制在内核态中执行的指令。这种机制叫系统调用, 在CPU中的实现称之为陷阱指令(Trap Instruction)。

用户态程序切换到内核态的流程如下:

  • 用户态程序将一些数据值放在寄存器中或者使用参数创建一个堆栈(stack frame),以此表明需要操作系统提供的服务
  • 用户态程序执行系统调用,即陷阱指令
  • CPU切换到内核态,并跳到位于内存指定位置的指令,这些指令是操作系统的一部分,他们具有内存保护,不可被用户态程序访问
  • 这些指令称之为陷阱(trap)或者系统调用处理器(system call handler), 他们会读取程序放入内存的数据参数, 并执行程序请求的服务
  • 系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果

JDK1.6对synchronized锁的优化

简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。然而在现实中的大部分情况下,同步方法是大多数是运行在单线程环境(无锁竞争),如果每次都调用Mutex Lock那么将严重的影响程序的性能。不过在JDK1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。

锁粗化(Lock Coarsening): 也就是减少不必要的紧连在一起的lock/unlock操作,将多个连续的锁扩展成一个范围更大的锁。

锁消除(Lock Elimination): 通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。

轻量级锁(Lightweight Locking): 这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒

偏向锁(Biased Locking): 比轻量级锁更轻,是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。

适应性自旋(Adaptive Spinning): 当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex lock)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的互斥锁进入到阻塞状态。

自旋锁不会引起调用者睡眠,如果自旋锁已经被别的单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋”一词就是因此而得名。

也就是说自旋锁就是一直在那里刷新,看看锁有没有被释放。而不是像传统的那种等待正在调用的线程释放锁后,然后通知这些等待的锁然后唤醒。

下面具体阐述JDK1.6是怎么实现偏向锁、轻级锁的以及锁怎样升级为重量级互斥锁的。

不过,在具体阐述之前,要先了解一下Java对象结构。

1、Java对象的创建与内存布局

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。 如果没有,那必须先执行相应的类加载过程。

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。 对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。因此,在使用Serial、ParNew等带Compact(整理)过程的收集器时,系统采用的分配算法是指针碰撞,因为上述垃圾收集算法运行后,空闲区域是连续的,内存碎片少。而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表

  • 指针碰撞:假设Java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把这个指针向空闲的内存那边挪动一段与对象大小相等的距离
  • 空闲列表:假设Java堆中的内存是不规整的,虚拟机就必须维护一个表,用来记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间分对象,并更新表上的记录

对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。这个时候主要有两个解决方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTlAB参数来设定。

Java对象由三部分构成:对象头、实例数据、对齐补充。

对象头

第一部分是与对象在运行时状态相关的信息,长度通过与操作系统的位数保持一致。包括对象的哈希值、GC分代年龄、锁状态以及偏向线程的ID等。由于对象头信息是与对象所定义的信息无关的数据,所以使用了非固定的数据结构,以便存储更多的信息,实现空间复用。因此对象在不同的状态下对象头的存储信息有所差别。
在这里插入图片描述
另一部分是类型指针,即指向该对象所属类元数据的指针,虚拟机通常通过这个指针来确定该对象所属的类型(但并不是唯一方式)。

另外,如果对象是一个数组,在对象头中还应该有一块记录数组长度的数据,因为JVM可以通过对象的元数据确定对象的大小,但不能通过元数据确定数组的长度。

实例数据

实例数据存储的是真正的有效数据,即各个字段的值。无论是子类中定义的,还是从父类继承下来的都需要记录。这部分数据的存储顺序受到虚拟机的分配策略以及字段在类中的定义顺序的影响。

对齐填充

这部分数据不是必然存在的,因为对象的大小总是8字节的整数倍,该数据仅用于补齐实例数据部分不足整数倍的部分,充当占位符的作用。

2、Java对象头存储锁数据

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)。要实现这个目标,则每个Java对象都应该与某种类型的锁数据关联。这就意味着,我们需要一个存储锁数据的地方,并且每一个对象都应该有这么个地方。在Java中,这个地方就是对象头

其实Java的对象头和对象的关系很像Http请求的http header和http body的关系。对象头中存储了该对象的Metadata, 除了该对象的锁信息,还包括指向该对象对应的类的指针、对象的hashcode、 GC分代年龄等,在对象头这个寸土寸金的地方,根据锁状态的不同,有些内存是大家公用的,在不同的锁状态下,存储不同的信息

synchronized是一种悲观锁,锁是存在对象头中的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit。

在这里插入图片描述
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:

在这里插入图片描述
synchronized代码块是由一对儿monitorenter/monitorexit 指令实现的,Monitor对象是同步的基本实现单元。在synchronized锁中,存储在对象头的Mark Word中的锁信息是一个指针,它指向一个Monitor对象(也称为管程或监视器锁)的起始地址。这样,我们就通过对象头,将每一个对象与一个Monitor关联了起来,它们的关系如下图所示:
在这里插入图片描述
图片的最左边是线程的调用栈,它引用了堆中的一个对象,该对象的对象头部分记录了该对象所使用的监视器锁,该监视器锁指向了一个Monitor对象。

那么这个Monitor对象是什么呢? 在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
 }

上面这些字段中,我们只需要重点关注三个字段:

  • _owner:当前拥有该 ObjectMonitor 的线程
  • _EntryList:当前等待锁的集合
  • _WaitSet:调用了Object.wait()方法而进入等待状态的线程的集合

在Java中,每一个等待锁的线程都会被封装成ObjectWaiter对象(ObjectWaiter类由JVM定义,ObjectWaiter对象里存放thread),当多个线程同时访问一段同步代码时,首先会被扔进 _EntryList 集合中,如果其中的某个线程获得了monitor对象,它将成为 _owner,如果在它成为 _owner之后又调用了wait()方法,则他将释放获得的Monitor对象,进入 _WaitSet集合中等待被唤醒

在这里插入图片描述

另外,因为每一个对象都可以作为synchronized的锁,所以每一个对象都必须支持wait()、notify()、notifyAll()方法,使得线程能够在一个Monitor对象上wait,直到它被notify。这也就解释了这三个方法为什么定义在了Object类中——这样,所有的类都将持有这三个方法。Object类的wait()和notify()都被标记为native方法,其具体实现都在JVM的synchronizer.cpp里。

所以说每一个Java对象都可以作为锁,其实是指将每一个Java对象所关联的ObjectMonitor作为锁,更进一步是指,大家都想成为某一个Java对象所关联的ObjectMonitor对象的_owner,所以你可以把这个_owner看做是铁王座,所有等待在这个监视器锁上的线程都想坐上这个铁王座,谁拥有了它,谁就有进入由它锁住的同步代码块的权利

3、Monitor锁在JDK1.6中多种实现

根据前面的分析,我们知道在Java 6之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。现代的(Oracle)JDK 中,JVM对此进行了大刀阔斧地改进,提供了三种不同的Monitor实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

所谓锁的升级、降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级

由于synchronized是JVM内部的Intrinsic Lock,所以偏斜锁、轻量级锁、重量级锁的代码实现,并不在核心类库部分,而是在JVM的代码中。

偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当没有竞争出现时,默认会使用偏向锁。当一个线程访问同步块并获取锁时,JVM会利用CAS操作(compareAndSwap),在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,所以并不涉及真正的互斥锁。只需要测试一下对象头的MarkWord里是否存储着当前线程ID,成功则表示线程已经获得了锁。

这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。偏向锁是一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程就会释放锁,即撤销偏向锁。JVM首先暂停拥有锁的线程,然后检查持有偏向锁的线程是否依然存活,若不再存活就将对象头设置成无锁状态;如果线程仍然存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

偏向锁获取与撤销流程如下图所示:
在这里插入图片描述

轻量级锁

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现(已经出现多个线程竞争锁,偏向锁的假设不再成立)。轻量级锁依赖CAS操作Mark Word来试图获取锁,如果CAS操作成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程栈桢中创建用于存储锁记录的空间。并将对象头中的Mark Word复制到锁记录中(官方称Displaced Mark Word),然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则当前线程获取锁,失败则代表其他线程竞争锁,当前线程便尝试使用自旋来获取锁

轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功则表示无竞争发生,如果失败代表锁存在竞争,锁进一步膨胀成重量级锁。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁(依赖操作系统提供的互斥量),就不会再恢复成轻量级锁的状态。当锁处于这个状态下,其他线程试图获取锁时都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争

多个线程争夺锁导致锁导致轻量级锁膨胀的流程图如下:
在这里插入图片描述

三种类型锁的优缺点对比如下:
在这里插入图片描述

Java对引用的定义

无论是通用引用计数算法判断对象的引用数据,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK1.2之前,Java中的引用定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。
强引用就是指在程序代码中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会
回收掉被引用的对象。

Java中根据其生命周期的长短,将引用分为4类。

强引用

特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。 当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。

软引用

特点:软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用

弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

应用场景:弱应用同样可用于内存敏感的缓存。

虚引用

特点:虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。

应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

关于强引用之外的另外三大引用,可以用以下代码来测试

public class TestReference {

    private static List<Object> list = new ArrayList<>();

    public static int M = 1024 * 1024;

    public static void main(String[] args) {
        testSoftReference();
        System.out.println("------------");
        list.clear();
        testWeakReference();
        System.out.println("------------");
        testPhantomReference();
    }

    private static void testSoftReference() {
        Runtime runtime = Runtime.getRuntime();
        String value = "我是软引用";
        SoftReference<String> sfRefer = new SoftReference<>(value);
        //可以获得引用对象值
        System.out.println(sfRefer.get());
        for (int i = 0; i < 10; i++) {
            byte[] buff = new byte[M];
            SoftReference<byte[]> sr = new SoftReference<>(buff);
            list.add(sr);
        }
        System.out.println(runtime.freeMemory() / M + "M(free) / " + runtime.maxMemory() / M + "M(max)");
        //主动触发垃圾回收
        System.gc();
		//垃圾回收后,再查看数据
        for (int i = 0; i < list.size(); i++){
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }
        //再申请4MB的空间,触发垃圾回收
        SoftReference<Object> softReference = new SoftReference<Object>(new byte[4 * M]);
        System.out.println("softReference.get() : " + softReference.get());
        for (int i = 0; i < list.size(); i++){
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }
    }

    private static void testWeakReference() {
        Runtime runtime = Runtime.getRuntime();
        String value = "我是弱引用";
        WeakReference<String> wkRefer = new WeakReference<>(value);
        //可以获得引用对象值
        System.out.println(wkRefer.get());
        //创建10MB数组
        for (int i = 0; i < 10; i++) {
            byte[] buff = new byte[M];
            WeakReference<byte[]> sr = new WeakReference<>(buff);
            list.add(sr);
        }
        System.out.println(runtime.freeMemory() / M + "M(free) / " + runtime.maxMemory() / M + "M(max)");
        //主动触发垃圾回收
        System.gc();
        //垃圾回收后,再查看数据
        for(int i = 0; i < list.size(); i++){
            Object obj = ((WeakReference) list.get(i)).get();
            System.out.println(obj);
        }

    }

    private static void testPhantomReference() {
        // 创建一个字符串对象
        String str = new String("Java的4大引用");
        // 创建一个引用队列
        ReferenceQueue rq = new ReferenceQueue();
        // 创建一个虚引用,让此虚引用引用到"疯狂Java讲义"字符串
        PhantomReference pr = new PhantomReference (str , rq);
        // 切断str引用和"Java的4大引用"字符串之间的引用
        str = null;
        // 取出虚引用所引用的对象,并不能通过虚引用获取被引用的对象,所以此处输出null
        System.out.println(pr.get());
        // 强制垃圾回收
        System.gc();
        System.runFinalization();
        // 垃圾回收之后,虚引用将被放入引用队列中
        // 取出引用队列中最先进入队列中的引用与pr进行比较
        System.out.println(rq.poll() == pr);
    }
}

在笔记本上运行效果如下,可以看到默认的JVM可用内存很大,垃圾回收时在内存空间足够用的情况下,软引用对象占用的空间不会被回收。

我是软引用
231M(free) / 3641M(max)
[B@60e53b93
[B@5e2de80c
[B@1d44bcfa
[B@266474c2
[B@6f94fa3e
[B@5e481248
[B@66d3c617
[B@63947c6b
[B@2b193f2d
[B@355da254
softReference.get() : [B@4dc63996
[B@60e53b93
[B@5e2de80c
[B@1d44bcfa
[B@266474c2
[B@6f94fa3e
[B@5e481248
[B@66d3c617
[B@63947c6b
[B@2b193f2d
[B@355da254
------------
我是弱引用
219M(free) / 3641M(max)
null
null
null
null
null
null
null
null
null
null
------------
null
true

Process finished with exit code 0

现在设置-Xms2M -Xmx15M,然后重新运行,效果如下:

我是软引用
1M(free) / 14M(max)
[B@60e53b93
[B@5e2de80c
[B@1d44bcfa
[B@266474c2
[B@6f94fa3e
[B@5e481248
[B@66d3c617
[B@63947c6b
[B@2b193f2d
[B@355da254
softReference.get() : [B@4dc63996
null
null
null
null
null
null
null
null
null
null
------------
我是弱引用
2M(free) / 14M(max)
null
null
null
null
null
null
null
null
null
null
------------
null
true

Process finished with exit code 0

软引用的实际应用

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

比如在图片加载框架中,通过软引用来实现内存缓存。

//实现图片异步加载的类
public class AsyncImageLoader {
    //以Url为键,SoftReference类型为值,建立缓存HashMap键值对。
    private Map<String, SoftReference<Drawable>> mImageCache = new HashMap<String, SoftReference<Drawable>>();
     
    //实现图片异步加载
    public Drawable loadDrawable(final String imageUrl, final ImageCallback callback) {
        //查询缓存,查看当前需要下载的图片是否在缓存中
        if(mImageCache.containsKey(imageUrl)) {
            SoftReference<Drawable> softReference = mImageCache.get(imageUrl);
            if (softReference.get() != null) {
                return softReference.get();
            }
        }
         
        final Handler handler = new Handler() {
            @Override
            public void dispatchMessage(Message msg) {
                //回调ImageCallbackImpl中的imageLoad方法,在主线(UI线程)中执行。
                callback.imageLoad((Drawable)msg.obj);
            }
        };
         
        /*若缓存中没有,新开辟一个线程,用于进行从网络上下载图片,
         * 然后将获取到的Drawable发送到Handler中处理,通过回调实现在UI线程中显示获取的图片
         */
        new Thread() {      
            public void run() {
                Drawable drawable = loadImageFromUrl(imageUrl);
                //将得到的图片存放到缓存中
                mImageCache.put(imageUrl, new SoftReference<Drawable>(drawable));
                Message message = handler.obtainMessage(0, drawable);
                handler.sendMessage(message);
            };
        }.start();
         
        //若缓存中不存在,将从网上下载显示完成后,此处返回null;
        return null;
    }
     
    //定义一个回调接口
    public interface ImageCallback {
        void imageLoad(Drawable drawable);
    }
     
    //通过Url从网上获取图片Drawable对象;
    protected Drawable loadImageFromUrl(String imageUrl) {
        try {
            return Drawable.createFromStream(new URL(imageUrl).openStream(),"debug");
        } catch (Exception e) {
            // TODO: handle exception
            throw new RuntimeException(e);
        }
    }
}

发布了92 篇原创文章 · 获赞 447 · 访问量 46万+

猜你喜欢

转载自blog.csdn.net/fuzhongmin05/article/details/104594894