前言

本篇文章次要学习synchronized关键字在JDK1.6引入的偏差锁轻量级锁,并围绕synchronized关键字的锁的降级进行展开讨论。本篇文章探讨的锁是通过synchronized加的锁,是不同于java.util.concurrent.locks.Lock的另外一种加锁机制,后续文中提及,均指synchronized关键字的锁。

参考资料:《Java并发编程的艺术》

注释

一. 锁的应用

synchronized能够用于润饰一般办法静态方法代码块,拜访被synchronized关键字润饰的内容须要先获取锁,获取的这个锁具体是什么,这里暂不探讨,上面先举例子来看一下synchronized关键字如何应用。

public class SynchronizedLearn {    //润饰一般办法    public synchronized void normalSyncMethod() {        ......    }    //润饰静态方法    public static synchronized void staticSyncMethod() {        ......    }    //润饰代码块    public void syncCodeBlock() {        synchronized (SynchronizedLearn.class) {            ......        }    }}

上述例子中,应用synchronized关键字润饰代码块时,传入了SynchronizedLearn类的类对象,实际上,synchronized关键字无论是润饰办法还是润饰代码块,均须要传入一个对象,咱们这里能够将传入的这个对象了解为,只不过在润饰办法时,会隐式地传入对象作为锁,规定如下。

  • 润饰一般办法时,隐式传入的对象为持有一般办法的实例对象自身;
  • 润饰静态方法时,隐式传入的对象为持有静态方法的类的类对象。

咱们称由synchronized关键字润饰的办法为同步办法,联合上述规定,对由synchronized关键字润饰的同步办法的拜访有如下留神点。

1. 实例对象的所有一般同步办法同一时刻只能由一个线程拜访

给出一个例子如下所示。

public class SynchronizedLearn {    public synchronized void normalSyncMethod1() {        ......    }    public synchronized void normalSyncMethod2() {        ......    }}

某时刻线程A和线程B都持有SynchronizedLearn的同一个实例synchronizedLearn,并且线程A胜利调用实例synchronizedLearnnormalSyncMethod1()办法,此时在线程A执行完normalSyncMethod1()办法以前,线程B都无法访问normalSyncMethod1()normalSyncMethod2()办法。

2. 类的所有动态同步办法同一时刻只能由一个线程拜访

给出一个例子如下所示。

public class SynchronizedLearn {    public static synchronized void staticSyncMethod1() {        ......    }    public static synchronized void staticSyncMethod2() {        ......    }}

某时刻线程A胜利调用SynchronizedLearn类的staticSyncMethod1()办法,此时在线程A执行完staticSyncMethod1()办法以前,线程B都无法访问staticSyncMethod1()staticSyncMethod2()办法。

3. 类的动态同步办法和类实例的一般同步办法同一时刻能够由不同线程拜访

给出一个例子如下所示。

public class SynchronizedLearn {    public synchronized void normalSyncMethod() {        ......    }    public static synchronized void staticSyncMethod() {        ......    }}

某时刻线程A持有SynchronizedLearn的实例synchronizedLearn并调用了其normalSyncMethod()办法,无论线程A是否执行完normalSyncMethod()办法,线程B都能够拜访SynchronizedLearn类的staticSyncMethod()办法。

二. 锁是什么

上一大节中提到:synchronized关键字无论是润饰办法还是润饰代码块,均须要传入一个对象,这个对象能够了解为锁。在JVM中通过monitorentermonitorexit指令来保障同步代码块的线程平安(同步办法是另外一种形式,由具体虚拟机决定其实现,这里不做探讨),monitorenter指令是在编译后插入到同步代码块的起始地位,monitorexit指令是在编译后插入到同步代码块的完结地位,当线程执行到monitorenter指令时,会尝试获取通过synchronized关键字传入的对象相关联的Monitor对象的所有权,即获取锁,当线程执行到monitorexit指令时,会放弃Monitor对象的所有权,即开释锁。

下面提到的Monitor是一种同步机制,个别由一个对象来实现,称之为Monitor对象,在Java中每一个Java对象都与一个Monitor对象相关联,具体的关联关系能够了解为共生共灭。在HotSpot虚拟机中,Monitor对象是ObjectMonitor对象,获取synchronized关键字传入的对象实际上就是获取和该对象关联的ObjectMonitor,其由c++实现,如下所示。

ObjectMonitor() {    _header       = NULL;    _count        = 0;    _waiters      = 0,    _recursions   = 0; //重入次数    _object       = NULL;    _owner        = NULL; //示意以后持有Monitor的线程    _WaitSet      = NULL; //期待队列    _WaitSetLock  = 0;    _Responsible  = NULL;    _succ         = NULL;    _cxq          = NULL;    FreeNext      = NULL;     _EntryList    = NULL; //同步队列    _SpinFreq     = 0;    _SpinClock    = 0;    OwnerIsThread = 0;}

ObjectMonitor对象中保护了两个队列,别离是_EntryList_WaitSet,其中_EntryList是同步队列,每一个想要获取ObjectMonitor对象所有权的线程会进入该队列,如果获取ObjectMonitor对象所有权失败,则阻塞在该队列中,直到胜利获取到ObjectMonitor对象所有权才会从_EntryList中移除,而_WaitSet是期待队列,当持有ObjectMonitor对象所有权的线程调用和ObjectMonitor对象关联的Java对象的wait()办法时,就会开释ObjectMonitor对象的所有权并进入_WaitSet队列中期待。那么到这里能够发现,Monitor对象和队列同步器AbstractQueuedSynchronizer很像,所以不太谨严的将synchronized的锁机制与java.util.concurrent.locks.Lock的锁机制进行一个比对:Java中的所有对象都是一把锁,能够和ReentrantLock这样的锁绝对应,而每个Java对象与一个Monitor对象相关联,能够和ReentrantLock这样的锁的自定义同步器绝对应。

三. 锁的降级

JDK1.6引入了偏差锁轻量级锁,用于解决synchronized关键字太过分量的问题。已知synchronized关键字润饰办法或者代码块时会传入对象作为锁,实际上synchronized关键字的锁存在于传入对象的对象头中,对于非数组类型的对象的对象头占用2个字宽,32/64位虚拟机中每字宽占用大小为32/64 bits,如下表所示。

长度内容项阐明
32/64 bitsMark Word存储对象hashCode或锁信息
32/64 bitsClass Metadata Address存储指向对象类型数据的指针

上表中的Mark Word在无锁状态下存储的数据为对象hashCode,在偏差锁轻量级锁重量级锁状态下存储的数据为锁信息,如下所示。

在不通过设置JVM参数被动敞开偏差锁的状况下偏差锁默认开启,并随着竞争的逐步强烈,锁会逐渐降级为轻量级锁重量级锁,在第二大节中提及的ObjectMonitor对象,理论就是锁收缩为重量级锁之后会应用到的数据。上面将对偏差锁,轻量级锁和重量级锁及其锁降级过程进行剖析。

1. 偏差锁

通常,锁被认定为不会存在多线程竞争,并且锁会被同一线程反复获取,这种状况下,锁会偏差某一个线程,即偏差锁。上面别离对偏差锁的获取偏差锁的重偏差偏差锁的撤销进行剖析。

1)偏差锁的获取和重偏差
一开始,锁对象Mark Word中的是否可偏差标记位为1,锁标记位为01,且线程ID为空,这示意锁可偏差且未偏差。如果线程判断锁是可偏差且未偏差状态,那么线程会基于CAS将锁对象Mark Word的线程ID字段设置为以后线程ID,如果设置胜利,示意胜利获取到偏差锁,如果设置失败,示意呈现了竞争,须要执行偏差锁的撤销逻辑。

如果锁对象Mark Word中的是否可偏差标记位为1,锁标记位为01,且线程ID不为空,此时获取锁的线程须要将锁对象Mark WordEpoch字段值与锁对象所属类的类信息中的Epoch字段值进行比拟,如果相等,则锁是可偏差且已偏差状态,如果不相等,则锁是可偏差且重偏差状态,依据不同的状态获取偏差锁的逻辑如下所示。

  • 如果是可偏差且已偏差状态,则判断锁对象Mark Word的线程ID字段是否和以后线程的线程ID相等,若相等,示意以后线程是锁偏差的线程,以后线程能够胜利获取偏差锁,若不相等,示意产生了锁竞争,此时须要执行偏差锁的撤销逻辑;
  • 如果是可偏差且重偏差状态,示意偏差锁能够被线程获取(等同于可偏差且未偏差状态),则以后线程能够以CAS形式来竞争获取偏差锁。

因为偏差锁不会有被动开释锁这个操作,所以某线程获取到偏差锁并执行完同步代码后,只管这个线程不再须要偏差锁,然而锁对象Mark Word的线程ID字段仍旧为这个线程的ID,所以偏差锁会应用锁对象Mark Word中的Epoch字段实现重偏差逻辑,即便得呈现上述情况时,另外的线程仍旧能够获取到偏差锁。Eopch字段的实现逻辑如下所示。

  • 偏差锁的锁对象Mark Word中会存储一份Epoch值,同时在偏差锁对象所属类的类信息中也会存储一份Epoch值;
  • 每达到全局平安点(该工夫点没有正在执行的字节码),类信息中的Epoch值会加1,失去Epoch_new,而后遍历类的所有实例对象,并判断这些对象是否还作为偏差锁被某个线程持有,如果是,则将Epoch_new赋值给对象Mark WordEpoch字段;
  • 当呈现锁对象Mark Word中的线程ID字段值不会空时,就将Mark WordEpoch字段值与类信息中的Epoch字段值进行比拟,如果相等,表明该锁还被某线程持有,如果不相等,则表明锁未被持有,该锁能够从新偏差某个线程。

3)偏差锁撤销
偏差锁的撤销,须要等到全局平安点,而后依据锁对象Mark Word中的线程ID找到被偏差的线程并暂停,再而后判断被偏差线程是否执行完同步代码,最初依据判断后果执行不同的偏差锁撤销逻辑,如下所示。

  • 如果被偏差线程曾经执行完同步代码,则锁会从偏差锁状态被置为无锁状态,此时锁对象Mark Word中的是否可偏差字段为0,示意不可偏差,此时的锁对象是不可偏差状态,后续如果须要获取该锁,间接以轻量级锁的形式获取;
  • 如果线程还未执行完同步代码,则偏差锁会间接降级为轻量级锁。

最初会唤醒在全局平安点被暂停的线程。

2. 轻量级锁

在偏差锁状态下,如果产生锁竞争,则偏差锁会降级为轻量级锁,或者通过设置JVM参数禁用偏差锁,获取锁时会间接获取轻量级锁。

1)轻量级锁的获取
线程执行同步代码前,若锁对象为无锁状态,则会在以后线程栈帧中创立用于存储锁记录(Lock Record)的内存空间,而后将锁对象的Mark Word复制到以后线程栈帧的锁记录中,称复制到锁记录中的Mark WordDisplaced Mark Word,同时锁记录中的owner指针会指向锁对象,再而后基于CAS形式将锁对象的Mark Word替换为指向以后线程栈帧锁记录的指针,如果替换胜利,示意胜利获取到轻量级锁,如果替换失败,则示意产生了竞争,以后线程进入自旋状态尝试获取锁。

锁记录的构造如下所示。

2)轻量级锁的重入
当线程要获取锁时,如果锁对象曾经是轻量级锁,此时判断锁对象头的指向线程栈帧锁记录的指针是否指向以后线程栈帧锁记录,如果不是,则示意其它线程持有该轻量级锁,则以后线程进入自旋状态尝试获取锁,如果是,则示意以后线程持有该轻量级锁,此时是锁重入

锁重入时,持有锁的线程会在线程栈帧又创立存储锁记录的内存空间,同时Displaced Mark Word为空,owner指针指向锁对象。每重入一次,就会创立一次Displaced Mark Word为空的锁记录内存空间,如下所示。

3)轻量级锁的降级
线程在获取轻量级锁时,有两种状况会获取失败并进入自旋获取锁的状态,如下所示。

  • 线程将锁对象的Mark Word复制到线程栈帧锁记录后,如果基于CAS形式将锁对象的Mark Word替换为指向以后线程栈帧锁记录的指针失败,此时会进入自旋获取锁的状态;
  • 线程获取锁时,如果锁对象是轻量级锁状态,但曾经被其它线程持有,此时会进入自旋获取锁的状态。

因为线程自旋获取锁会耗费CPU资源,所以不会让自旋获取锁的线程始终自旋,当自旋达到肯定次数时,会判断自旋获取锁失败,此时会让轻量级锁降级为重量级锁,重量级锁的锁对象Mark Word会变成指向锁对象关联的ObjectMonitor对象的指针,锁标记位会置为10,同时阻塞自旋获取锁失败的线程。

4)轻量级锁的开释
持有轻量级锁的线程退出同步代码时会开释轻量级锁,会基于CAS形式将线程栈帧锁记录的Displaced Mark Word替换回到锁对象的Mark Word中,如果替换胜利,表明胜利开释轻量级锁,如果替换失败,表明产生了锁竞争且锁曾经降级为重量级锁,此时线程开释锁并唤醒在重量级锁上期待的线程。

3. 重量级锁

当锁从轻量级锁状态降级为重量级锁状态后,此时锁是一种乐观锁,所有竞争获取锁失败的线程会被阻塞,直到持有锁的线程开释锁则会又退出对锁的竞争中。

重量级锁的锁对象的Mark Word为指向锁对象关联的ObjectMonitor对象的指针,重量级锁的同步语义也是依赖ObjectMonitor对象来实现,该局部内容在第二节中曾经进行了剖析,这里不再反复探讨。

总结

synchronized关键字的锁有四种状态:无锁状态偏差锁状态轻量级锁状态重量级锁状态,这四种状态会随着竞争的逐步强烈而顺次降级。应用偏差锁是为了升高锁总是会被同一线程反复获取场景下获取锁的开销,应用轻量级锁是为了让竞争获取锁的线程不被阻塞从而防止线程用户态和内核态之间切换的耗费,而当锁降级到重量级锁后,此时乐观地认为锁资源竞争很强烈,每个竞争锁资源失败的线程间接被阻塞从而防止自旋获取锁时对CPU资源的耗费