zoukankan      html  css  js  c++  java
  • Synchronized

    三种应用方式

    - 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
    - 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
    - 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
    

    字节码指令

    - javap -v 来查看对应代码的字节码指令,对于同步块的实现使用了monitorenter和monitorexit指令
    - 同一时刻只能有一个线程获取到由synchronized所保护对象的监视器
    

    原理

    jdk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁

    前提须知:

    • 对象头
      - 对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充
      - java对象头是实现synchronized的锁对象基础,使用的锁对象存储在java对象头中。
      - 是轻量级锁和偏向锁的关键
    
    • Mark Word(标记字)
     - Mark word 用于存储对象自身的运行数据。
     - 哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
     - Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit)
    

    锁的转换

    锁的级别从低到高逐步升级, 无锁->偏向锁->轻量级锁->重量级锁

    我们通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。但是重量级锁会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。
    为了提高效率,不会一开始就使用重量级锁,JVM在内部会根据需要,按如下步骤进行锁的升级:
       1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。
       2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,
         把该线程当做自己的熟人。如下图第二种情形。
       3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。
       4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会通过一个对象内部的监视器(monitor)来登记和管理排队的线程,
         其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。如下图第四种情形。
    

    img

    • monitor
      我们可以把它理解为一个同步工具,也可以描述为一种同步机制。所有的Java对象是天生的
      Monitor,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象
      在hotspot虚拟机中,通过ObjectMonitor类来实现monitor。他的锁的获取过程的体现会简单很多
    

    扩展:

    自旋锁(CAS)

      自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起。看持有锁的线程是否能够很快释放锁。怎么自旋呢?其实就是一段没有任何意义的循环。
      虽然它通过占用处理器的时间来避免线程切换带来的开销,但是如果持有锁的线程不能在很快释放锁,那么自旋的线程就会浪费处理器的资源,因为它不会做任何有意义的工作。所以,自旋等待的时间或者次数是有一个限度的,
    如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起
    

    wait和notify

      调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,
    并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。
    

    题外话:

    wait和notify为什么需要在synchronized里面?

      wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
      对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。
    
  • 相关阅读:
    多线程下的调用上下文 : CallContext
    MongoDB入门实战教程(7)
    MongoDB入门实战教程(6)
    MongoDB入门实战教程(5)
    MongoDB入门实战教程(4)
    MongoDB入门实战教程(3)
    MongoDB入门实战教程(2)
    MongoDB入门实战教程(1)
    ASP.NET 5 with Dapr 初体验
    2021成都.NET开发者Connect线下活动
  • 原文地址:https://www.cnblogs.com/snail-gao/p/11761808.html
Copyright © 2011-2022 走看看