看完你就会知道,如果一个线程锁定了一个资源,其他线程无法访问的锁就叫做一个悲观锁,反之,线程不锁定资源的锁称为乐观锁,自旋锁是基于CAS机制实现的,CAS是乐观锁的一种实现。那么对于锁,多个线程访问某个资源的过程细节是否相同?也就是说,当多个线程同步访问一个资源时,锁的状态会发生怎样的变化呢?本文对此进行讨论。锁状态的分类Java语言专门为synchronized关键字设置了四种状态,分别是:无锁、偏向锁、轻量级锁和重量级锁,但在了解这些锁之前,需要了解Java对象头和Monitor。Java对象头我们知道synchronized是悲观锁。在操作同步之前,需要锁定资源。这个锁在对象头,什么是Java对象头?我们以Hotspot虚拟机为例。Hopspot对象头主要包括两部分数据:MarkWord(标记字段)和KlassPointer(类型指针)。MarkWord:默认存储对象的HashCode、世代年龄和锁标志位信息。这些信息是与对象本身的定义无关的数据,所以MarkWord被设计成一个非固定的数据结构,在很小的空间内存中存储尽可能多的数据。它会根据对象的状态重用自己的存储空间,也就是说,在运行过程中,MarkWord中存储的数据会随着锁标志的变化而变化。KlassPoint:对象指向它的类元数据指针,虚拟机通过这个指针来判断对象是哪个类的实例。32位虚拟机和64位虚拟机的MarkWord占用的字节大小不同。32位虚拟机的MarkWord和KlassPointer分别占用32位字节,而64位虚拟机的MarkWord和KlassPointer占用64位字节,我们以32位虚拟机为例看看它的MarkWord的字节是如何分配的。中文翻译就是stateless的意思,就是在没有锁的情况下,对象头开辟25bit空间用来存放对象的hashcode,4bit用来存放世代年龄,1bit用来存放锁是否偏向的标识位,2bit用来存放锁标识位是01。偏向锁的划分比较细,还是开辟25bit空间,其中23bit用来存放线程ID,2bit用来存放epoch,4bit用来存放世代年龄,1bit用来存放是否偏向锁,0表示不锁,1表示偏向锁,锁标志还是01,30bit直接开在轻量级锁栈中存放指向锁记录指针的空间,2bit用于存放锁的标志位,其标志位为00。重量级锁与轻量级锁相同。30bit空间用于存放指向重量级锁的指针,2bit存放锁30bit内存空间分配给11GCmark但未被占用,2bit空间用于存放锁标志位为11。其中无锁和偏向锁的锁标志位都是01,但是第一个1位区分是无锁状态还是偏向锁状态。至于为什么要这样分配内存,我们可以从OpenJDK中markOop.hpp类中的枚举得到一个线索来解释age_bits就是我们所说的分代回收标志,占用4个字节。lock_bits为锁的标志位,占用2个字节biased_lock_bits为锁是否偏向的标识,占用1个字节max_hash_bits为无锁计算的hashcode占用的字节数,如果是32位虚machine,就是32-4-2-1=25字节,如果是64位虚拟机,64-4-2-1=57字节,但是会有25字节没有用到,所以64位的hashcode占用31字节hash_bits是64位虚拟机,如果最大字节数大于31,则取31,否则取真实字节数cms_bits我觉得如果是64位应该是0字节虚拟机,如果是64位则为1byteepoch_bits为epoch占用的字节大小,2bytes。同步锁synchronized使用的锁保存在Java对象头中。JVM根据进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的。monitorenter指令在编译后插入同步代码块的开头,而monitorexit则插入在方法的末尾和异常处。任何对象都有一个与之关联的监视器,当和一个监视器被持有时,它就会被锁定。根据虚拟机规范的要求,在执行monitorenter指令时,首先尝试获取对象的锁。如果对象没有被锁定,或者当前线程已经拥有该对象的锁,则锁计数器加1。相应地,当执行monitorexit指令时,锁计数器减1。当计数器减到0,锁被释放。如果获取对象锁失败,则当前线程会阻塞等待,直到对象锁被其他线程释放。MonitorSynchronized是通过对象内部的一个监视器锁(monitor)来实现的,监视器锁的本质是依赖底层操作系统的MutexLock(互斥锁)来实现的。操作系统需要从用户态切换到核心态,才能实现线程间的切换。这个成本非常高,状态之间的转换需要比较长的时间,这也是Synchronized效率低下的原因。因此,这种依赖于操作系统MutexLock的锁被称为重量级锁。为了减少获取和释放锁带来的性能消耗,JavaSE1.6引入了偏向锁和轻量级锁:锁有4种状态,级别从低到高:无锁状态、偏向锁状态、轻量级lock量级锁状态和重量级锁状态。锁可以升级但不能降级。所以锁状态一共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁升级是单向的,也就是说只能从低级升级到高级,有将无锁失败降级)。在JDK1.6中,默认启用了偏向锁和轻量级锁。我们也可以通过-XX:-UseBiasedLocking=false来禁用偏向锁。锁的分类及其解释Lock-free和lock-free的状态,lock-free是指资源没有被锁定,所有线程都可以访问同一个资源,但只有一个线程可以成功修改资源。无锁的特点是修改操作是在循环中进行的。线程会不断尝试修改共享资源,直到修改资源成功退出。过程中没有冲突,和我们上一篇介绍的CAS很像。CAS的实现原理和应用就是无锁实现。Lock-free不能完全替代lock,但是在某些场合下lock-free的性能是非常高的。BiasedLockHotspot的作者通过研究发现,在大多数情况下,不仅不存在多线程竞争锁的情况,还会出现同一个线程多次获取锁的情况。偏向锁就出现在这种情况下。它的出现是为了仅在一个线程执行同步时提高性能。从对象头的分配可以看出,偏向锁的线程ID和epoch比没有锁的要多。当一个线程访问同步代码块并获得锁时,它会将线程ID存储在对象头和栈帧记录中。当线程下次进入和退出同步代码块时,不需要进行CAS操作来加锁和解锁。只需要简单判断对象头的MarkWord中是否存储了指向当前线程的线程ID即可。判断标志当然是根据锁的标志位来判断的。偏向锁的获取过程访问MarkWord查看偏向锁的flag是否设置为1,锁的flag是否为01---确认为偏向状态。如果确认为可偏置状态,则判断当前线程ID与对象头中存储的线程ID是否一致。如果一致,转第5步,不一致,转第3步。如果当前线程ID与对象头If中保存的线程ID不一致,则使用CAS操作来竞争锁。如果竞争成功,则修改MarkWord中的线程ID为当前线程ID,然后执行第5步,否则执行第4步。获取偏向锁,说明至少有其他线程获取了偏向锁,因为该线程不会主动释放偏向锁)。当到达全局安全点(SafePoint)时,它会先挂起持有偏向锁的线程,然后检查持有偏向锁的线程是否存活(因为持有偏向锁的线程可能已经执行完毕,但线程会不主动去释放偏向锁),如果线程不活跃,将对象头设置为无锁状态(标志位为01),然后重新偏向新线程;如果线程还活着,则取消偏向锁,升级为轻量级锁状态(标志位为00),此时轻量级锁由原来持有偏向锁的线程持有,继续执行其同步代码,而竞争线程将进入自旋并等待获得轻量级锁。在同步代码中执行偏向锁的释放过程。偏向锁的释放过程可以参考上面的第4步,当偏向锁遇到其他线程竞争锁时,持有偏向锁的线程会释放锁,线程不会主动释放偏向锁。偏向锁的取消需要等待全局安全点(此时没有字节码在执行),会先挂起拥有偏向锁的线程,判断锁是否被锁,恢复到取消偏向锁(标志位为01)或轻量级锁(标志位为00)后解锁。禁用偏向锁Java6和Java7默认启用偏向锁。由于偏向锁是为了在只有一个线程执行同步块时提高性能,如果确定应用程序中所有的锁通常都处于竞争状态,可以通过JVM参数:-XX:-UseBiasedLocking=false关闭偏向锁,那么程序默认会进入轻量级锁状态。关于epoch对epoch这个概念的理解比较复杂。这里简单的理解一下,就是epoch的值可以作为时间戳来检测偏向锁的有效性。被线程访问,偏向锁将升级为轻量级锁,其他线程会尝试以自旋的形式获取锁,不会阻塞,从而提高性能。在加锁过程中,当代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志为01,是否为偏向锁为0),虚拟机会先创建一个当前线程栈帧中的名称。是锁记录(LockRecord)的空间,用来存放锁对象当前MarkWord的副本,然后将对象头中的MarkWord复制到锁记录中。复制成功后,虚拟机会使用CAS操作尝试更新对象的MarkWord为指向LockRecord的指针,并将LockRecord中的owner指针指向对象的MarkWord。如果更新动作成功,则该线程拥有该对象的锁,对象MarkWord的锁标志位设置为00,表示该对象处于轻量锁状态。如果更新操作失败,虚拟机会先检查对象的MarkWord是否指向当前线程的栈帧。如果是,说明当前线程已经拥有了该对象的锁,那么可以直接进入同步块继续执行。否则,意味着多个线程竞争锁,轻量级锁将扩展为重量级锁。锁标志的状态值变为10,指向重量级锁(mutex)的指针存放在MarkWord中,等待后面的锁。线程也进入阻塞状态。重量级锁重量级锁通常称为同步对象锁,锁标志位为10,指针指向监控对象(也称为监视器或监控锁)的起始地址。每个对象都有一个与之关联的监视器,并且有许多方法可以实现对象与其监视器之间的关系。例如,监视器可以与对象一起创建和销毁,也可以在线程尝试获取对象锁时自动生成。但是,当监视器一旦被线程持有时,它就会被锁定。上图简单描述了多线程获取锁的过程。当多个线程同时访问一段同步代码时,会先进入EntrySet。当线程获得对象的管程后,进入Owner区,将管程中的owner变量设置为当前线程。同时,monitor中的counter计数加1,如果线程调用wait()方法,当前持有的monitor会被释放,owner变量恢复为null,count减11、同时线程进入WaitSet集合等待被唤醒。如果当前线程执行完毕,也会释放管程(锁)并重置变量的值,以便其他线程进入并获取管程(锁)。从这个角度来看,monitor对象存在于每个Java对象的对象头中(指向存储的指针),同步锁就是通过这种方式获得的,这就是Java中任何对象都可以作为锁的原因。同时也是顶层对象Object存在notify/notifyAll/wait等方法的原因。
