前言
本篇文章次要学习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胜利调用实例synchronizedLearn的normalSyncMethod1()
办法,此时在线程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
中通过monitorenter和monitorexit指令来保障同步代码块的线程平安(同步办法是另外一种形式,由具体虚拟机决定其实现,这里不做探讨),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 bits | Mark Word | 存储对象hashCode或锁信息 |
32/64 bits | Class 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 Word的Epoch
字段值与锁对象所属类的类信息中的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 Word的Epoch
字段; - 当呈现锁对象Mark Word中的线程ID字段值不会空时,就将Mark Word的
Epoch
字段值与类信息中的Epoch
字段值进行比拟,如果相等,表明该锁还被某线程持有,如果不相等,则表明锁未被持有,该锁能够从新偏差某个线程。
3)偏差锁撤销
偏差锁的撤销,须要等到全局平安点,而后依据锁对象Mark Word中的线程ID找到被偏差的线程并暂停,再而后判断被偏差线程是否执行完同步代码,最初依据判断后果执行不同的偏差锁撤销逻辑,如下所示。
- 如果被偏差线程曾经执行完同步代码,则锁会从偏差锁状态被置为无锁状态,此时锁对象Mark Word中的是否可偏差字段为0,示意不可偏差,此时的锁对象是不可偏差状态,后续如果须要获取该锁,间接以轻量级锁的形式获取;
- 如果线程还未执行完同步代码,则偏差锁会间接降级为轻量级锁。
最初会唤醒在全局平安点被暂停的线程。
2. 轻量级锁
在偏差锁状态下,如果产生锁竞争,则偏差锁会降级为轻量级锁,或者通过设置JVM参数禁用偏差锁,获取锁时会间接获取轻量级锁。
1)轻量级锁的获取
线程执行同步代码前,若锁对象为无锁状态,则会在以后线程栈帧中创立用于存储锁记录(Lock Record)的内存空间,而后将锁对象的Mark Word复制到以后线程栈帧的锁记录中,称复制到锁记录中的Mark Word为Displaced 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资源的耗费
。