并发锁机制synchronized

注意:了解synchornized之前,必须要了解并发编程的三大特性:及时可见性,有序性,原子性,其中可见性,有序性可以通过java关键字 volatile 实现,volatile原理在下一篇文章中介绍

1.synchronized的两种用法

  第一,修饰方法,通过ACC_SYNCHRONIZED标识。

    

  第二,修饰方法体,通过monitorenter和monitorexit两个jvm指令实现

 

   这里有两个moniterExit,第一个moniterexit是正常退出,第二个moniterexit是考虑到异常的情况。

2.Moniter(监视器/管程)  

  Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。
在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

3.MESA模型

  在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。

 

 

 

  管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。
  对于MESA管程来说,有一个编程范式:
  

 

    while(condition){

      wait();

   }

  
  唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait(timeout)方法还有一个超时参数,为了避免线程进入等待队列永久阻塞。
  wait()释放锁之后,需要notify()/notifyAll();  
  1. 所有等待线程拥有相同的等待条件;
  2. 所有等待线程被唤醒后,执行同样的操作
  3. notify()只能唤醒一个线程   

  满足以上条件,可以使用notify,其余时候请使用notifyall();

4.java synchornized实现的管程

  java参考MESA模型,自己内置了管程的实现synchornized,在MESA中,有多个的条件队列,在java实现的管程只有一个条件队列。器模型图如下

 

 java对于Monitor的实现,主要在java.lang.Object中,在该类中定义了wait(),notify(),notifyall()方法,这些具体方法的实现,依赖于c++实现的ObjectMonitor类,

ObjectMonitor其主要数据结构如下(hotspot源码ObjectMonitor.hpp):
ObjectMonitor() {
    _header       = NULL; //对象头  markOop
    _count        = 0;  
    _waiters      = 0,   
    _recursions   = 0;   // 锁的重入次数 
    _object       = NULL;  //存储锁对象
    _owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) 
    _WaitSet      = NULL;  // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;    
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
...

ObjectMonitor定义了三个队列

  _WaitSet:等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点,

  _EntryList:存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程);

  _cxq:多线程竞争锁会先存到这个单向链表中 (FILO栈结构)

  其关系如下:

 

 java线程唤醒额默认策略:

  在获取锁时,是将当前线程插入到cxq的头部(栈结构头插法),而释放锁时,默认策略是:当EntryList为空,则将cxq中的元素按照原顺序插入到EntryList中,并且唤醒一个线程,  也就是当EntryList为空时,是后来的线程向北唤醒(因此,synchornized是非公平锁),ExtryList不为空时,从ExtryList中唤醒。

 

synchronized加锁加在对象上,锁对象是如何记录锁状态的?

  了解这个问题,首先需要学习对象在内存中的布局:
对象在内存中主要分为3个部分:对象头,示例数据,对其填充。
  对象头:存放hashcode,对象分代年龄,锁标识,偏向锁Id,偏向时间,数组长度(数组对象独有)
  实例数据:对象的属性
  对其填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐(可以理解为jvm对象寻址的最优解决方案)。
   

 

 

对象头详解
  mark word:用于存储运行时数据,比如:hashcode,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit(8字节),官方称它为“Mark Word”。

  Klass Pointer:即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。jdk1.8默认开启指针压缩功能,当堆内存<32G时,占4个字节,否则,占8个字节,所以一般建议堆内存不超过32G;

  数组长度:数组对象独有

对象头的布局可以通过JOL(java Object layout)工具查看,比如:

  • OFFSET:偏移地址,单位字节;
  • SIZE:占用的内存大小,单位为字节;
  • TYPE DESCRIPTION:类型描述,其中object header为对象头;
  • VALUE:对应内存中当前存储的值,二进制32位;
Mark Word是如何记录锁状态的
  Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。
简单点理解就是:MarkWord 结构搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。
  32位系统对象头结构

   64位系统对象头结构

 

 

 

synchornized锁标识的理解

 

  下面通过JOL来追踪锁的变化过程:

  •  无锁:对象在创建时是无锁的
  •  

     

  • 偏向锁:偏向锁是针对锁的一种优化手段,为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁,比如StringBuffer的append方法,
jdk1.6默认启用偏向锁,当一个对象新创建时,此时markword中的threadid位0,此时,表明此时处于可偏向但是为偏向的状态,也叫匿名偏向锁。
  • 偏向锁延迟偏向

jvm虚拟机默认在启动 4s后,为每个新建的对象开启偏向锁模式。

  //关闭延迟开启偏向锁 -XX:BiasedLockingStartupDelay=0
  //禁止偏向锁 -XX:-UseBiasedLocking //启用偏向锁
验证
  

 

 

偏向锁撤销:
  调用对象hashcode,如果此时处于可偏向但是未偏向状态,那么将升级为轻量级锁,偏向锁无法保存对象hashcode

 

 偏向锁在偏向过程中,调用对象hashcode,此时,将升级为重量锁

 

当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向:
  • 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。

偏向锁撤销之notify()/wait()

调用wait()

 

 

偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁
 
轻量级锁
  倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。(不会自旋  底层是if判断  不是 for循环)
轻量级锁膨胀为重量级锁
  会调用park,切换到内核态 
。。。持续更新
posted @ 2022-03-15 23:52  转身瞬间  阅读(180)  评论(0编辑  收藏  举报