1.前言锁一共有四种,等级从低到高依次为:无锁、偏向锁、轻量级锁、重量级锁。这四种锁的状态分别代表什么,为什么会有锁升级?其实在JDK1.6之前,synchronized还是一个重量级的锁,是一个比较低效的锁。不过在JDK1.6之后,Jvm对paired(synchronized)进行了优化,引入了偏向锁和轻量级锁。自此,出现了四种锁状态(无锁、偏向锁、轻量级锁、重量级锁),这四种状态将跟随竞争。情况是逐渐升级的,而且是一个不可逆的过程,即不能降级,也就是说只能进行锁升级(从低级到高级),锁降级(从高级到低级))无法执行,这意味着锁倾向于升级为轻量级锁,不能降级为偏向锁。这种锁升级不降级策略的目的是为了提高获取和释放锁的效率。2.synchronized中锁的四种状态最初的实现是“阻塞或唤醒一个Java线程需要操作系统切换CPU状态才能完成。这个状态切换需要处理器时间。如果同步代码块的内容太简单了,这个切换时间可能比用户代码的执行时间还长”。该方法是synchronized同步的初始方法。这也是开发者诟病的地方。这也是JDK6之前synchronized效率低下的原因。获取和释放锁带来的性能消耗引入了“偏向锁”和“轻量级锁”。因此,目前有四种锁状态,从低到高依次为:无锁、偏向锁、轻量级锁、重量级锁。锁状态只能升级,不能降级。如图:3.LockState思想及特点4.锁比较5.synchronized锁synchronized使用的锁存在于Java对象头中,那么什么是对象头呢?5.1Java对象头我们以Hotspot虚拟机为例。Hopspot对象头主要包括两部分数据:MarkWord(标记字段)和KlassPointer(类型指针)MarkWord:默认存储对象的HashCode、代数年龄和锁标志位信息。这些信息是与对象本身的定义无关的数据,所以MarkWord被设计成一个非固定的数据结构,在很小的空间内存中存储尽可能多的数据。它会根据对象的状态重用自己的存储空间,也就是说,在运行过程中,MarkWord中存储的数据会随着锁标志的变化而变化。KlassPoint:对象指向它的类元数据指针,虚拟机通过这个指针来判断对象是哪个类的实例。上面我们知道synchronized使用的锁存在于Java对象头中,那么它存在于对象头的什么位置呢?答案是:锁对象的对象头中有一个MarkWord,那么对象头中的MarkWord有多长呢?它存储什么?在64位虚拟机中:在32位虚拟机中:我们以32位虚拟机为例,看看MarkWord的字节是如何分配的无锁:在对象头中分配了25bits来存放对象的hashcode,4bits用于存储对象的世代年龄,1bits用于存储是否偏向锁的标志,2bits用于存储锁标志为01偏向锁:在偏向锁中lock里面划分的更细一些,还是开辟25bit的空间,其中23bit用来存放线程ID,2bit用来存放Epoch,4bit用来存放对象的分代年龄,1bit存放是否加锁是偏向的,0表示不加锁,1表示偏向锁,锁标志还是01轻量级锁:直接在轻量级锁中开辟30bit空间存放栈中锁记录的指针,2bit存放锁标志,anditsflagis00重量级锁:重量级锁中的锁与lig相同轻量级锁。30位空间用于存放指向重量级锁的指针,2位空间用于存放锁标志位,标记为11GC:30位内存空间被打开但未被占用,2位空间用于存放锁标志。11.其中无锁和偏向锁的lockflags都是01,只是前面的1位区分是无锁状态还是偏向锁状态。关于内存分配,我们可以在git中openJDK的markOop.hpp中看到:public://Constantsenum{age_bits=4,lock_bits=2,biased_lock_bits=1,max_hash_bits=BitsPerWord-age_bits-lock_bits-biased_lock_bits,hash_bits=max_hash_bits>31?31:max_hash_bits,cms_bits=LP64_ONLY(1)NOT_LP64(0),epoch_bits=2};age_bits:就是我们所说的分代恢复的标志,占4个字节lock_bits:是锁的标志位,占2个字节biasedlockbits:是锁是否偏向的标志,占1个字节无锁计算的hashcode占用字节,如果是32位虚拟机,则为32-4-2-1=25字节,如果是64位虚拟机,则为64-4-2-1=57字节,但是会有25字节没有使用,所以64位的hashcode占用31字节hash_bits:对于64位虚拟机,如果最大字节数大于31,取31,否则取实际numberofbytescms_bits:not64-bitvirtualepoch_bits:是epoch占用的字节大小,2字节。5.2MonitorMonitor可以理解为同步工具或者同步机制,通常描述为一个对象。每个Java对象都有一个不可见的锁,称为内部锁或监视器锁。Monitor是一个线程私有的数据结构。每个线程都有一个可用的监控记录列表,还有一个全局的可用列表。每个加锁的对象都关联一个监视器,监视器中有一个Owner字段,用于存储拥有该锁的线程的唯一标识,表示该锁被该线程占用。Synchronized是通过对象内部的一个监视器锁(monitor)来实现的,监视器锁的本质是依赖底层操作系统的MutexLock(互斥锁)来实现的。操作系统需要从用户态切换到核心态,才能实现线程间的切换。这个成本非常高,状态之间的转换需要比较长的时间,这也是Synchronized效率低下的原因。因此,这种依赖于操作系统MutexLock的锁被称为重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁升级是单向的,也就是说只能从低级升级到高级,有将无锁失败降级)。在JDK1.6中,默认启用了偏向锁和轻量级锁。我们也可以通过-XX:-UseBiasedLocking=false来禁用偏向锁。6、锁的分类6.2Lock-freeLock-free是指没有资源被锁定,所有线程都可以访问和修改同一个资源,但只有一个线程可以修改成功。无锁的特点是修改操作会循环进行,线程会不断尝试修改共享资源。如果没有冲突,则修改成功并退出,否则继续循环。如果有多个线程修改同一个值,一定有一个线程可以修改成功,而其他修改失败的线程会不断重试,直到修改成功。6.3偏向锁当synchronized代码块第一次执行时,锁对象就变成了偏向锁(通过CAS修改了对象头中的锁标志),字面意思是“偏向第一个线程”的锁获得它”。执行完同步代码块后,线程不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断持有锁的线程是否是自己(持有锁的线程ID也在对象头中),如果是则继续正常执行。由于之前没有释放过锁,所以这里不需要重新加锁。如果自始至终只有一个线程在使用锁,显然几乎没有偏向锁的额外开销,性能极高。偏向锁是指当一段同步代码始终被同一个线程访问时,即多个线程之间不存在竞争时,则该线程在后续访问时会自动获取锁,从而减少获取锁的消耗,即提高性能。当线程访问同步代码块并获得锁时,它会将偏向锁的线程ID存储在MarkWord中。当线程进入和退出同步块时,不再通过CAS操作加锁和解锁,而是检测MarkWord中是否存在指向当前线程的偏向锁。轻量级锁的获取和释放依赖多条CAS原子指令,而偏向锁在替换ThreadID时只需要依赖一条CAS原子指令。只有当其他线程试图竞争偏向锁时,持有偏向锁的线程才会释放锁,该线程不会主动释放偏向锁。关于偏向锁的取消,需要等待全局安全点,即当某个时间点没有字节码正在执行时,会先挂起拥有偏向锁的线程,然后判断锁定对象是否被锁定。如果线程不处于活动状态,对象头设置为无锁状态,并撤销偏向锁,状态恢复为无锁(标志位为01)或轻量级锁(标志位为00)。6.4轻量级锁(自旋锁)轻量级锁是指当锁为偏向锁时,由另一个线程访问。此时偏向锁会升级为轻量级锁,其他线程会自动尝试以自旋的形式获取锁(自旋的介绍见文末),线程不会被阻塞,从而提高性能。轻量级锁的获取主要由两种情况引起:①当偏向锁功能关闭时;②由于多个线程竞争偏向锁,偏向锁升级为轻量级锁。一旦第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里需要明确什么是锁竞争:如果多个线程轮流去获取锁,但是每次获取锁都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当线程试图获取锁,发现锁已经被占用,只能等待释放时,才会发生锁竞争。在轻量级锁状态下,锁的竞争还在继续,没有抢到锁的线程会自旋,也就是不断循环判断能否成功获取到锁。获取锁的操作其实就是通过CAS修改对象头中的锁标志。首先比较当前的锁标志是否为“已释放”,如果是则设置为“已锁定”。比较和设置自动发生。这样就认为抢到了锁,然后线程修改当前锁持有者信息给自己。长时间的自旋操作非常耗费资源。如果一个线程持有锁,其他线程只能就地消耗CPU,无法执行任何有效任务。这种现象称为忙等待。如果多个线程使用一个锁,但没有发生锁竞争,或者发生非常轻微的锁竞争,那么synchronized使用轻量级锁来允许短暂的忙碌。这是一个折衷的想法,短暂的忙等待,换取线程在用户态和内核态之间切换的开销。6.4重量级锁重量级锁显然,这种忙等待是有限制的(有一个计数器记录自旋次数,默认允许10个循环,可以通过虚拟机参数更改)。如果锁竞争严重,达到最大自旋数的线程会将轻量级锁升级为重量级锁(CAS仍然修改锁标志,但不修改持有锁的线程ID)。当后续线程尝试获取锁,发现占用的锁是重量级锁时,会直接挂起自己(而不是忙等待),等待以后被唤醒。重量级锁是指当一个线程获取到锁时,所有其他等待获取锁的线程都会被阻塞。总之,所有的控制权都交给了操作系统,操作系统负责线程间的调度,改变线程的状态。而这样会频繁的切换线程运行状态,挂起和唤醒线程,从而消耗大量的系统资源。5.小结文章介绍了锁的四种状态,以及锁是如何一步步升级的。
