JVM学习总结——十、JVM与synchronize锁

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

偏向锁
轻量级锁
重量级锁

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


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

对象头包含两部分:

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


类型指针
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。
该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存。
选项+UseCompressedOops开启指针压缩,开启该选项后,下列指针将压缩至32位。

压缩指针的例如:

每个Class的属性指针(即静态变量)
每个对象的属性指针 (即对象变量)
普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化例如:
1. 指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)
2. 本地变量
3. 堆栈元素、入参、返回值和NULL指针等。

数组长度
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
 

Mark Word  
1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该     对象处于无锁状态(无线程竞争它)。

2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图第二种情形。

3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。

4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。如下图第四种情形。

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

  • Monitor

Monitor是 synchronized 重量级 锁的实现关键。锁的标识位为 10 。当然 synchronized作为一个重量锁是非常消耗性能的,所以在JDK1.6以后做了部分优化,接下来的部分是讲作为重量锁的实现。

Monitor是线程私有的数据结构,每一个对象都有一个monitor与之关联。每一个线程都有一个可用monitor record列表(当前线程中所有对象的monitor),同时还有一个全局可用列表(全局对象monitor)。每一个被锁住的对象,都会和一个monitor关联。

当一个monitor被某个线程持有后,它便处于锁定状态。此时,对象头中 MarkWord的 指向互斥量的指针,就是指向锁对象的monitor起始地址
monitor是由 ObjectMonitor 实现的,其主要数据结构如下:(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

object monitor 有两个队列 _EntryList_WaitSet ,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象)_owner 指向持有 objectMonitor的线程。

当多个线程同时访问一个同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor后,会进入_owner 区域,然后把monitor中的 _owner 变量修改为当前线程,同时monitor中的计数器_count 会加1。

根据虚拟机规范的要求,在执行monitorenter指令时,会尝试获取对象的锁。如果对象没有被锁定(获取锁),获取对象已经被该线程锁定(锁重入)。则把计数器加1(_count 加1)。相应的,在执行monitorexit指令时,会讲计数器减1。当计数器为0时,_owner指向Null,锁就被释放。(摘自《深入理解JAVA虚拟机》)

如果线程调用 wait() 方法,将释放当前持有的monitor,_owner变量恢复为null,_count变量减1,同时该线程进入_WaitSet 等待被唤醒。


锁状态和对应的对象头存储内容

jvm的三种锁

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的值,否则不执行更新。上述过程是一个原子操作。

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

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

重量级锁

关于更深入的 synchronized实现原理,以及JVM对锁性能的优化 - 简书

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术分子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值