JVM学习总结——JVM与锁

jvm中有以下三种锁(由上到下越来越“重量级”):
偏向锁
轻量级锁
重量级锁

其中重量级锁是最初的锁机制,偏向锁和轻量级锁是在jdk1.6加入的,可以选择打开或关闭。
如果把偏向锁和轻量级锁都打开,那么在java代码中使用synchronized关键字的时候,jvm底层会尝试先使用偏向锁,如果偏向锁不可用,则转换为轻量级锁,如果轻量级锁不可用,则转换为重量级锁。

这3种锁需要了解对象的内存结构(MarkWord头),会涉及到字节码的内部存储格式,需要了解两个大体的概念:

对象头包含两部分:
1.Makr Word 用于存储对象自身运行数据(哈希码,GC分代年龄,锁状态标志,线程持有锁偏向线程ID,偏向时间戳)
2.类型指针,对象指向它类型元数据指针,JVM通过指针确定该对象是个类实例。
对象如果是java数组对象头还有一块记录数组长度数据。

Lock Record: 即锁记录,每个线程在执行的时候,会有自己的虚拟机栈,当个方法的调用相当于虚拟机栈里的一个栈帧,而Lock Record就位于栈帧上,是用来保存关于这个线程的加锁信息。

jvm的三种锁在第1、2点的实现上本质上是一样的:

1.在共享数据里保存一个锁 java同步是通过synchronized关键字实现的。
synchronized有三种用法:
一种是同步块,这种用法需要指明一个锁定对象;
二种是修饰静态方法,这种用法相当于锁定Class对象;
三种是修饰普通方法,这种用法相当于锁定方法所在的实例对象。
因此,在java里能够被synchronized关键字锁定的一定是对象,因此就要在对象里保存一个锁,而对象内存结构里的MarkWord就可以认为是这个锁。
三种锁虽然实现细节不同,但是都是使用MarkWord保存锁的。

2.在锁里保存这个线程的标识 
偏向锁是在对象的MarkWord里保存线程id
轻量级锁是在对象的MarkWord里保存指向拥有锁的线程栈中锁记录的指针
重量级锁是在对象的MarkWord中保存指向互斥量的指针(互斥量只向一个线程授予对共享资源的独占访问权,可以认为是记录了线程的标识)

3.其他线程访问已加锁共享数据要等待锁释放

重量级锁因为使用了互斥量,这里的等待就是线程阻塞。使用互斥量可以保证所有情况下的并发安全,但是使用互斥量会带来较大的性能消耗。
等到发现有并发的时候再使用互斥量呢?答案是可以的,轻量级锁和偏向锁都是基于这种假设来实现的。

三种锁能保证变量只有一个线程访问
偏向锁最快但是只能用于从始至终只有一个线程获得锁
轻量级锁较快但是只能用于线程串行获得锁
重量级锁最慢但是可以用于线程并发获得锁,先用最快的偏向锁,每次假设不成立就升级一个重量。

轻量级锁

轻量级锁的核心思想就是“被加锁的代码不会发生并发,如果发生并发,那就膨胀成重量级锁(膨胀指的锁的重量级上升,一旦升级,就不会降级了)”。
轻量级锁依赖了一种叫做CAS(compare and swap)的操作,这个操作是由底层硬件提供相关指令实现的:
CAS操作需要3个参数,分别是内存位置V,旧的期望值A和新值B。CAS指令执行时,当且仅当V当前值符合旧值A时,处理器用新值B更新V的值,否则不执行更新。上述过程是一个原子操作。

轻量级锁加锁
假设现在开启了轻量级锁,当第一个线程要锁定对象时,该线程首先会在栈帧中建立Lock Record(锁记录)的空间,用于存储对象目前MarkWord的拷贝。
然后虚拟机将使用CAS操作尝试将对象的MarkWord更新为指向线程锁记录的指针。
如果操作成功,则该线程获得对象锁。
如果失败,说明在该线程拷贝对象当前MarkWord之后,执行CAS操作之前,有其他线程获取了对象锁,我们最开始的假设“被加锁的代码不会发生并发”失效了。
此时轻量级锁还不会直接膨胀为重量级锁,线程会自旋不停地重试CAS操作寄希望于锁的持有线程主动释放锁,在自旋一定次数后如果还是没有成功获得锁,那么轻量级锁要膨胀为重量级锁。
成功获取了轻量级锁的那个线程现在依旧持有锁,只是换成了重量级锁,其他尝试获取锁的线程进入等待状态。

轻量级锁解锁
轻量级锁的解锁也是用CAS来操作,如果对象的MarkWord中依然是持有锁线程的锁记录指针,则CAS成功,把锁记录中的原MarkWord的拷贝复制回去,解锁完成;
如果对象的MarkWord中保存的不再是持有锁线程的锁记录指针,说明在持有锁线程持有锁期间,这个轻量级锁已经因为其它线程并发获取膨胀为了重量级锁,
因此线程在释放锁的同时,还要唤醒(notify)等待的线程。

偏向锁

根据轻量级锁的实现遇到“并发”就要膨胀为重量级锁,但是轻量级锁可以支持多个线程以串行的方式访问同一个加锁对象。
比如A线程可以先获取对象o的轻量锁,然后A释放了轻量锁,这个时候B线程来获取o的轻量锁,是可以成功获取得,以这种方式可以一直串行下去。
之所以能实现这种串行,是因为有一个释放锁的动作。那么假设有一个加锁的java方法,这个方法在运行的时候其实从始至终只有一个线程在调用,但是每次调用完却也要释放锁,下次调用还要重新获得锁。

“假设加锁的代码从始至终就只有一个线程在调用,如果发现有多于一个线程调用,再膨胀成轻量级锁也不迟”。这个假设,就是偏向锁的核心思想。

核心实现
偏向锁的核心实现很简单:
假设开启了偏向锁,当第一个线程尝试获得对象锁的时候,也会在栈帧中建立Lock Record锁记录,但是这个Lock Record空间不需要初始化(后面会用到它),
然后直接用CAS将自己的线程ID写到对象的MarkWord里,如果CAS操作成功,就获取了偏向锁。线程获取偏向锁后即便是执行完加锁的代码块,也会一直持有锁不会主动释放。
因此这个线程以后每次进入这个锁相关的代码块的时候,都不需要执行任何额外的同步操作。

当有另外一个线程尝试获得锁的时候,需要进行revoke操作,分情况讨论:

判断持有偏向锁的线程是否还活着,如果线程不处于活动状态,则偏向锁被重置为无锁状态。
如果持有偏向锁的线程还活着而且当前线程实际没有持有着锁,则偏向锁被重置为无锁状态。
如果持有偏向锁的线程还活着而且当前线程实际持有着锁(在同步代码块中),那么试图获得偏向锁的线程将等待一个全局安全点(global safepoint),
在全局安全点,【试图获得偏向锁的线程】操作【持有偏向锁的线程的线程栈】,遍历里面的所有栈帧里的所有与当前锁对象相关联的LockRecord,
修改LockRecord里的内容为轻量级锁的LockRecord应该有的内容,然后把“最老的”(oldest)一个LockRecord的指针写到对象的MarkWord里,至此,就好像是原来从没有使用过偏向锁,使用的一直是轻量级锁。

一个已经持有偏向锁的线程,再次进入这个锁相关的代码块的时候,虽然不需要执行额外的同步操作,但是依旧会在栈上生成一个空的LockRecord,
因此对于一个重入了几次对象锁的线程来说,栈中就有了关联同一个对象的多个LockRecord。
而且jvm运行时里,会记录着加锁的次数,每重入一次,就+1;
当每次要解锁的时候,首先会把加锁次数-1,只有当加锁次数减到0的时候,才真正的去执行加锁操作。
而加锁次数减到0的时候,此时对应的锁记录肯定是第一次加锁的锁记录,也就是“最老的”,因此需要把“最老的”锁记录的指针写到对象的MarkWord里,这样当执行轻量级锁解锁的CAS操作的时候就能够成功解锁了。)