
前言本篇文章主要学习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 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 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资源的消耗。