理解JVM(5)锁

来源:互联网 发布:百科题库 软件下载 编辑:程序博客网 时间:2024/06/08 20:13

对象头和锁

JVM的实现中,每个对象都有一个对象头,用于保存对象的系统信息。对象头中有一个Mark Word部分,里面存放对象的哈希值,对象的年龄,锁的指针,是否占用锁,哪个锁等信息

这里写图片描述

在32位系统中,Mark Word占32位,这是小端储存,从右往左看。默认状态下,对象前2位总是状态位,第三位表示是否是偏向锁,看得到不是一定有的。无锁,第4-6位会存放对象年龄,8-32位放Hash值。

这里写图片描述

//  32 bits://  --------//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)//             size:32 ------------------------------------------>| (CMS free block)//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)////  64 bits://  --------//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)//  size:64 ----------------------------------------------------->| (CMS free block)////  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

锁在JVM中的优化

为了避免在操作系统层面的挂起线程,JVM自己优先解决问题,办法有n种

这里写图片描述

偏向锁

产生的原因是,大多数时候加锁只是一个保护性的措施,大多数时候并不会出现竞态。而真正出现了竞态情况,才会退出偏向模式。

启用参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0(表示马上启动,默认在4秒后)。在竞态环境强的时候,频繁进退偏向模式会消耗时间,可以禁用偏向锁优化。

我理解的原因是,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

这里写图片描述

轻量锁

如果偏向锁失败,JVM会让线程申请轻量锁。轻量锁在JVM内部是以BasicObjectLock的对象来实现的.其中有lockField和objField,该对象存在于线程私有的Java栈里面。而BasicLock类中存放对象头部的Mark Word的备份。

public class BasicObjectLock extends VMObject {  private static sun.jvm.hotspot.types.Field    lockField;  private static sun.jvm.hotspot.types.OopField objField;  private static int        size;}public class BasicLock extends VMObject {  private static CIntegerField displacedHeaderField;  public Mark displacedHeader() {    return new Mark(addr.addOffsetTo(displacedHeaderField.getOffset()));  }}//C++部分实现markOop mark = obj->mark();lock->set_displaced_header(mark);if (mark == (markOop) Atomic::cmpxchg_ptr(lock,obj()->mark_addr(),mark)){    TEVENT (show_enter: release stacklock);}

首先BasicLock通过set_displaced_header()方法备份了原对象的Mark Word.然后通过CAS,尝试将BasicLock的地址复制到对象头的Mark Word中。如果成功,则加锁成功,否则加锁失败。加锁失败可能会被膨胀为重量级锁。

这里写图片描述

锁膨胀->重量级锁

当轻量级锁失败,会膨胀为重量级锁.第1步是废弃前面BasicLock备份的对象头信息,第2步是通过inflate()方法进行锁膨胀,获取对象的ObjectMoniter,然后再通过enter()尝试进入该锁,在enter()方法中可能会在操作系统层面挂起线程,成本就会比较高。

lock->set_displaced_header(markOopDesc::unused_mark());ObjectSynchronizer::inflate(THREAD, obj()) -> enter(THREAD)

自旋锁

在锁膨胀后,在操作系统挂起线程之前,JVM会做最后一次争取避免被操作系统挂起,这种操作被称为自旋锁。

自旋锁可以使线程在没有取得锁时,不被挂起,而转为执行一个空循环,执行若干个循环后,能获取锁最好,不能则由操作系统挂起。

在JDK1.6时可以通过-XX:UseSpinning开启自旋锁,使用-XX:PreBlockSpin参数设定自旋锁等待次数。在JDK1.7中完全交给JVM,它自动执行,不能控制。

public class SpinLock {      private AtomicReference<Thread> sign =new AtomicReference<>();      public void lock(){        Thread current = Thread.currentThread();        while(!sign .compareAndSet(null, current)){        }      }      public void unlock (){        Thread current = Thread.currentThread();        sign .compareAndSet(current, null);      }    }

锁消除

这是JVM在JIT编译阶段,通过上下文扫描,和逃逸分析,去除不可能出现竞态的锁。如像Vector,StringBuffer这些类。

参数:逃逸分析:+XX:+DoEscapeAnalysis,锁消除:+XX:EliminateLocks。必须在-server模式下才行。

其他措施

减小锁粒度:ConcurrentHashMap分了16段

锁分离:LinkedBlockingQueue有取放两把锁

锁粗化:减少获取锁的次数

CAS:Compare and Swap,atomic包

LongAddr:分段+CAS,更快的AtomicLong

volatile

保证了像Long,Double这种64位操作的原子性

保证了有序性,指令不会重排序

保证了可见性,强制CPU从内存读,而不是缓存

Happens-Before原则

指令重排序时候,不会违背这些原则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile的写先于读,保证可见性
  • 锁规则:解锁必须发生于加锁前
  • 传递性:a先于b,b先于c,则a先于c
  • 线程的start()先于它其他动作
  • 线程所有动作先于线程的结束Thread.join()
  • 线程的中断(interrupt())先于被中断的线程的代码
  • 对象的constructor()早于finalize()