理解JVM(5)锁

对象头和锁

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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值