并发编程读书笔记(三)synchronized 和 四种锁状态

    技术2022-07-11  88

    在多 线 程并 发编 程中 synchronized 一直是元老 角色,很多人都会称呼它 重量 级锁 。但 是,随着 Java SE 1.6 synchronized 行了各种 化之后,有些情况下它就并不那么重了。   先来看下利用 synchronized 实现 同步的基 Java 中的每一个 象都可以作 为锁 。具体表 以下 3 种形式。 · 于普通同步方法, 是当前 象。 · 于静 同步方法, 是当前 Class 象。 · 于同步方法 Synchonized 括号里配置的 象。     Synchonized JVM 里的 实现 原理, JVM 基于 入和退出 Monitor对象 {     //TODO   了解管程 ( )    每一个对象和类都与一个监视器相关联    要线程独占某块数据/代码(SpecialRoom)  ,那么先进入Hallway等待,然后调度器基于某些规则(如先进先出)从Hallway中取一个线程,若线程处于被挂起状态 那么就把它送进等待房间,过一段时间再送进 SpecialRoom。    而Monitor (监视器)的作用就是 保证 仅一个线程访问受保护的数据/代码 在Java中 每个对象和类都有一个监视器与之关联。为了实现监视器的互斥功能,锁(有时候也称为互斥体)与每一个对象和类关联。在操作系统书中,这叫做信号量,互斥锁也被称为二元信号量。

        如果一个线程拥有某些数据上的锁,其他线程想要获得锁只能等到这个线程释放锁。如果我们在进行多线程编程时总是需要编写一个信号量,那就不太方便了。幸运的是,我们不需要这样做,因为JVM会自动为我们做这件事。

    JVM会实现一个信号量,来与每个类/对象的监视器关联。

        为了声明一个同步区域(这里意味着数据不可能被超过一个线程访问),Java提供了synchronized块和synchronized方法。一旦代码被synchronized关键字绑定,它就是一个监视器区域。它的锁将会在后面被JVM实现。

    }   来实现 方法同步和代 码块 同步,但两者的 实现细节 不一 。代 码块 同步是使用 monitorenter monitorexit 指令 实现 的,而方法同步是使用另外一种方式 实现 的, 细节 JVM 范里并没有 详细说 明。但是,方法的同步同 可以使用 两个指令来 实现   monitorenter 指令是在 编译 后插入到同步代 码块 的开始位置,而 monitorexit 是插入到方法 和异常 JVM 要保 每个monitorenter对应monitorexit与之配    

    Java

    在pom.xml中 加入 <dependency>     <groupId>org.openjdk.jol</groupId>     <artifactId>jol-core</artifactId>     <version>0.9</version> </dependency>

    虽然看包名出处是openjdk,但在OpenJDK和Oracle JDK上都可以使用

    用法:

    System.out.println(ClassLayout.parseInstance(user).toPrintable()); 32位虚拟机下的MarkWord

    打印synchronize锁住的对象信息和没有被锁的对象信息

        可以看到 对象被锁住时 头部第一行最后两位变成了01  即被重量级锁锁定。   64位虚拟机下的MarkWord { 存疑    System.out.println(System.getProperty("sun.arch.data.model")); 通过这个方法可以得知我的jvm是64位 但是为什么markword按照32位来的?? }      

    锁的升级和对比

    Java SE 1.6 了减少 锁带 来的性能消耗,引入了 偏向 级锁 ,在 Java SE 1.6中, 一共有 4 种状 级别 从低到高依次是:无 、偏向级锁状态和重量级锁 几个状 会随着 争情况逐 可以升但不能降 级   {   偏向锁 级锁 后不能降 成偏向 锁  ,但是偏向锁可以降成无锁   }    却不能降 的策略,目的是 了提高获得 的效率(后续介绍)    

    偏向锁:

    HotSpot [1] 的作者 经过 研究 发现 ,大多数情况下, 不存在多 线 争,而且 是由同 一线 程多次 得, 让线 的代价更低而引入了偏向 当一个 线 访问 同步 并获取 锁时 ,会在 栈帧 中的 锁记录 里存 储锁 偏向的 线 ID ,以后 该线 程在 入和退出 同步 块时 不需要CAS操作来加和解 只需 简单 测试 一下 Mark Word 里是否存储 着指向当前线程的偏向           如果 测试 成功,表示 线 程已 经获 得了           如果 测试 ,                         需要再测试 一下 Mark Word 中偏向 标识是否置成1 (表示当前是偏向 ):                                    如果没有 置, 则使用CAS ;(实际上就是用无锁方式中的CAS)                                      如果 置了, 则尝试 使用 CAS 的偏向 指向当前 线 程。    

    偏向的撤

    偏向 使用了一种等到 争出 的机制,所以当其他 线 尝试竞 争偏向 锁时 , 持有偏向锁 线 程才会 偏向 的撤 ,需要    等待全局安全点{ 这个来保证线程安全 } (在 时间 点上没有正在执 行的字 节码 )。 它会首先 有偏向线 程,然后 检查 持有偏向 线 程是否活着,           如果线 程不 于活 头设 置成无            如果 线 程仍然活着, 有偏向 栈会被执 行,遍 偏向 象的 锁记录 中的 锁记录 Mark Word 要么重新偏向于其他线程,要么恢复到无 或者 标记对 象不适合作 偏向 ,最后 停的 线 程。     偏向锁的初始化流程  

    偏向

    偏向 Java 6 Java 7 里是默 启用的,但是它在 用程序启 几秒 之后才激活, 如有必要可以使用JVM 参数来关 -XX:BiasedLockingStartupDelay=0 如果你确定 用程 序里所有的锁 通常情况下 争状 ,可以通 JVM 参数关 偏向             -XX:UseBiasedLocking=false,那么程序默 级锁  

    轻量级锁

    级锁

    线 程在 行同步 之前, JVM 会先在当前 线 程的 栈桢 建用于存 储锁记录 的空 ,并 中的 Mark Word 复制到 锁记录 中,官方称 Displaced Mark Word 然后 线 尝试 使用CAS将 中的 Mark Word 换为 指向 锁记录 的指 。如果成功,当前 线 ,如果失 ,表示其他 线 ,当前 线 程便 尝试 使用自旋来  

    级锁

    锁时 ,会使用原子的 CAS 操作将 Displaced Mark Word 回到 ,如果成 功, 表示没有 生。如果失 ,表示当前 存在 争, 就会膨 成重量 级锁     争夺锁导致的锁膨胀流程图 (线程1 :我忙活了一圈白干了是吧 )       

    轻量级锁和偏向锁的区别 : 

    偏向锁没有复制对象头MarkWord到当前线程的栈帧,而是通过将对象头中的 线程id指向自己  , 轻量级锁则是将MarkWord替换为指向锁记录的指针。长度是 30bit   最后i两位是00  偏向锁 在MarkWord倒数第三位会 用1 表示自己是偏向锁   最后两位是01  

    相同点 

    都使用了 CAS操作 偏向锁使用CAS操作替换MarkWord中的线程ID  而 轻量级锁 则是 比较 线程程中复制的DisplayMarkWord 和 对象的MarkWord 。    

    偏向锁 轻量级锁  重量级锁 优缺点

                           
    Processed: 0.011, SQL: 9