java锁synchronized底层实现,synchronized重量级锁实现原理

  java锁synchronized底层实现,synchronized重量级锁实现原理

  00-1010 1.轻型锁的原理2。轻型锁的分类1。普通旋转锁2。自适应旋转锁3。轻质船闸问题:扩展综述

  什么是旋转锁?

  说说同步底层实现的原理?

  多线程中同步锁升级的原理是什么?

  00-1010引入轻量级锁的主要目的是在多线程竞争不激烈的情况下,通过CAS机制减少竞争锁对重量级锁造成的性能损失。重量级锁使用操作系统底层的互斥锁,会导致线程在用户模式和核心模式之间频繁切换,造成较大的性能损失。

  轻量级锁的使用场景:如果一个对象有多个线程要锁,但是锁的时间是错开的(即没有竞争),那么可以使用轻量级锁进行优化。

  轻量级锁的目的是尽量不要在操作系统级别使用互斥锁,因为它们的性能相对较差。线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一个沉重的负担。同时我们可以发现很多对象锁的锁定状态只会持续很短的时间,比如整数的自加操作。短时间内阻塞和唤醒线程显然是不值得的。因此,引入了轻量级锁。轻质锁是旋转锁的一种。因为JVM本身就是一个应用,所以希望在应用层面解决线程同步的问题。

  public class Main { static final Object obj=new Object();public static void main(String[]args){ Thread Thread=new Thread(()-{ method 1();});thread . start();} public static void method 1(){ synchronized(obj){//synchronize block A method 2();}}公共静态void方法2(){ synchronized(obj){//同步块b } } }轻量级锁的执行过程:

  在抢锁线程进入临界段之前,如果内置锁没有被锁定,JVM会先在抢锁线程的堆栈帧中建立一个锁记录,用来存储对象标记字的副本,

  然后抢锁线程会使用CAS spin操作,尝试将内置锁对象头的Mark Word的ptr_to_lock_record(锁记录指针)更新为抢锁线程堆栈帧中锁记录的地址。如果这个更新成功执行,这个线程将拥有这个对象锁。然后JVM将标志字中的锁标志位改为00(轻量锁标志),表示对象处于轻量锁状态。成功抢到锁后,JVM会将原来的锁对象信息(如hash码)保存在抢锁线程的锁记录的位移标志字字段中,然后将抢锁线程中锁记录的所有者指针指向锁对象。

  锁记录是线程私有的,每个线程都有自己的锁记录。创建锁记录后,带有内置锁对象的标记字将被复制到锁记录的移位标记字字段。这是为什么呢?因为内置锁对象的MarkWord的结构会发生变化,Mark Word会有一个指向锁记录的指针,而不是存储解锁状态下锁对象的hash码等信息,所以这些信息必须临时存储,以备锁释放时使用。

  在(1) 的抢锁线程进入临界段之前,如果内置锁没有被锁定,JVM会先在抢锁线程的堆栈帧中建立一个锁记录,每个线程的堆栈帧都会包含一个锁记录结构,其中可以存储被锁定对象的标志字。

  (2)抢锁线程会使用CAS spin操作,尝试将内置锁对象头的mark word的ptr_to_lock_record(锁记录指针)更新为抢锁线程堆栈帧中锁记录的地址。如果此更新成功,该线程将拥有对象锁。然后jvm将标记字中的锁标志位更改为00,这意味着对象处于轻量级锁状态。

  成功抢到锁后,jvm会保存原来的锁对象信息(如哈希代码等。)在锁抓取线程的锁记录的移位标志字字段中,然后将锁抓取线程中锁记录的所有者指针指向锁对象。

  64位的mark word结构如表所示:

   style="text-align:center">

  在轻量级锁抢占成功之后,锁记录和对象头的状态如图所示:

  

 

  锁记录是线程私有的,每个线程都有自己的一份锁记录,在创建完锁记录后,会将内置锁对象的Mark Word复制到锁记录的Displaced Mark Word字段。这是为什么呢?因为内置锁对象的mark word的结构会有所变化,mark word将会出现一个指向锁记录的指针,而不再存着无锁状态下的锁对象哈希码等信息,所以必须将这些信息暂存起来,供后面在锁释放时使用。

  (3) 如果 cas 失败,有两种情况:

  如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 ;如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数;

 

  (4) 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

  

 

  (5) 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 mark word的值恢复给对象头

  成功,则解锁成功失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

  

 

  

2. 轻量级锁的分类

轻量级锁主要有两种:普通自旋锁和自适应自旋锁。

 

  

 

  

1、普通自旋锁

所谓普通自旋锁,就是指当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这个抢锁线程才可以获得锁。

 

  说明:

  锁在原地循环等待的时候是会消耗CPU的,就相当于在执行一个什么也不干的空循环。所以轻量级锁适用于临界区代码耗时很短的场景,这样线程在原地等待很短的时间就能够获得锁了。默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin选项来进行更改。

  

 

  

2、自适应自旋锁

所谓自适应自旋锁,就是等待线程空循环的自旋次数并非是固定的,而是会动态地根据实际情况来改变自旋等待的次数,自旋次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。自适应自旋锁的大概原理是:

 

  如果抢锁线程在同一个锁对象上之前成功获得过锁,jvm就会认为这次自旋很有可能再次成功,因此允许自旋等待持续相对更长的时间。如果对于某个锁,抢锁线程很少成功获得过,那么jvm将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。自适应自旋解决的是锁竞争时间不确定的问题。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定。总的思想是:根据上一次自旋的时间与结果调整下一次自旋的时间。

  JDK 1.6的轻量级锁使用的是普通自旋锁,且需要使用-XX:+UseSpinning选项手工开启。

  JDK 1.7后,轻量级锁使用自适应自旋锁,JVM启动时自动开启,且自旋时间由JVM自动控制。

  轻量级锁也被称为非阻塞同步、乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待。

  

 

  

3. 轻量级锁的膨胀

轻量级锁的问题在哪里呢?

 

  虽然大部分临界区代码的执行时间都是很短的,但是也会存在执行得很慢的临界区代码。临界区代码执行耗时较长,在其执行期间,其他线程都在原地自旋等待,会空消耗CPU。因此,如果竞争这个同步锁的线程很多,就会有多个线程在原地等待继续空循环消耗CPU(空自旋),这会带来很大的性能损耗。

  轻量级锁的本意是为了减少多线程进入操作系统底层的互斥锁的概率,并不是要替代操作系统互斥锁。所以,在争用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的重量级锁。

  如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有 竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  (1) 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

  

 

  这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为锁对象申请 Monitor 锁,让锁对象指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

  

 

  当 Thread-0 退出同步块解锁时,使用 cas 将mark word的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。

  

 

  

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注盛行IT的更多内容!

 

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: