精华内容
下载资源
问答
  • 锁升级过程
    千次阅读 多人点赞
    2021-08-25 19:47:05

       1.说到锁升级的过程,我们就得说一下对象头

               

            对象头

                   java对象保存在内存中,由3个部分组成:

                         1.    对象头

                         2.   实例数据

                         3.   对齐填充字节

                         4.    如果是数组还包含数组长度

            对象头的存在形式

                           让我们先看看图,主要来说一下 Mark Word 

               

                    

    • markword 8bytes
    • class pointer - 指向对象所属的class (一般是4bytes)
    • instance data - 成员变量
    • padding - 8字节对齐

     

     Mark Word里都有啥

    hashcode
    GC

    为了让你们更好理解我先放一张图

        

    此处,有几点要注意:

    • 如果对象没有重写hashcode方法,那么默认是调用os::random产生hashcode,可以通过System.identityHashCode获取;os::random产生hashcode的规则为:next_rand = (16807seed) mod (2*31-1),因此可以使用31位存储;另外一旦生成了hashcode,JVM会将其记录在markword中;
    • GC年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15;
    • 当处于轻量级锁、重量级锁时,记录的对象指针,根据JVM的说明,此时认为指针仍然是64位,最低两位假定为0;当处于偏向锁时,记录的为获得偏向锁的线程指针,该指针也是64位;

    这里呢,是讲解的锁升级所以就重点看一下后两位,锁状态的判断就是看后两位的状态,无锁和偏向锁是看倒数第三位的状态 

    接下来让我们看看锁升级的过程

    专业版解释 

    1、当没有被当做锁的时候,这就是个普通对象,锁标志位为01,是否偏向锁为0

    2、当对象被当做同步锁时,一个线程A抢到锁时,锁标志位依然是01,是否偏向锁为1,前23位记录A线程的线程ID,此时锁升级为偏向锁

    3、当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码,这也是偏向锁的意义

    4、当一个线程B尝试获取锁,JVM发现当前的锁处于偏向状态,并且现场ID不是B线程的ID,那么线程B会先用CAS将线程id改为自己的,这里是有可能成功的,因为A线程一般不会释放偏向锁。如果失败,则执行5

    5、偏向锁抢锁失败,则说明当前锁存在一定的竞争,偏向锁就升级为轻量级锁。JVM会在当前线程的现场栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁MarkWord中保存指向这片空间的指针。上面的保存都是CAS操作,如果竞争成功,代表线程B抢到了锁,可以执行同步代码。如果抢锁失败,则继续执行6

    6、轻量级锁抢锁失败,则JVM会使用自旋锁,自旋锁并非是一个锁,则是一个循环操作,不断的尝试获取锁。从JDK1.7开始,自旋锁默认开启,自旋次数由JVM决定。如果抢锁成功,则执行同步代码;如果抢锁失败,则执行7

    7、自旋锁重试之后仍然未抢到锁,同步锁会升级至重量级锁,锁标志位改为10,在这个状态下,未抢到锁的线程都会被阻塞,由Monitor来管理,并会有线程的park与unpark,因为这个存在用户态和内核态的转换,比较消耗资源,故名重量级锁
    ————————————————
    详情请看:https://blog.csdn.net/wyb_gg/article/details/107518521

    我通过马士兵老师讲的带味道的栗子大致懂了这个过程(菜鸟版理解)

    首先呢,小马去上厕所噗噗噗,但是这个厕所很特殊,门上是没有锁的(无锁状态)

    小马觉得这不太安全啊,于是就想了个办法,上厕所噗噗噗的时候先贴上自己的名字,这样是不是就不会遇到尴尬的事(偏向锁)

    但是这样还是不好,要是方圆百里只有这一个厕所,翠花和小李都想上厕所怎么办,这时候就发生了锁竞争,他们会通过一个叫CAS来抢这个厕所,他们中有可能成功,把自己的名字贴到厕所门上,那如果没成功呢???

    没成功就会升级成轻量级锁,jvm会在当前线程的现场栈开辟一块空间,让翠花和小李在那里转圈圈的抢着谁上厕所(也叫自旋)也是通过CAS来实现的,  自旋的次数是10次以上,或者CPU核数的一半(JDK1.7开始,自旋锁默认开启,自旋次数由JVM决定) 那如果又失败了呢!  俩孩子快拉裤兜子了!!!!!!

    这时候就会升级成重量级锁,重量级这个词一听就不一般,JVM说:我头快秃了,干不了了。所以,我们的重量级锁是os老大哥管理的(希望老板也能对我好点o(╥﹏╥)o)。

    注:

    1.CAS中呢,底层是lock cmpxchg(大家不会的话可以自行百度)CAS也有很多问题:就像ABA啥的,这里就不多bb了

    2.那么有的小小猿就会问了,啥时候变成匿名对象呢?是4s以后才会加上偏向锁,变成匿名对象滴,那么咋取消呢,-XX:-UseBiasedLocking 或者去sleep

    这就是我对锁升级的理解,如果有错误的话,还望指正(热爱学习的好孩子)!!!

    更多相关内容
  • 主要介绍了Java synchronized锁升级jol过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • synchronized锁升级过程

    2022-03-13 18:30:10
    从理论到实操,详细介绍了的底层升级过程并用代码进行验证,加深记忆

    一.锁升级理论.

    在synchronized锁升级过程中涉及到以下几种锁.先说一下这几种锁是什么意思.

    偏向锁:只有一个线程争抢锁资源的时候.将线程拥有者标识为当前线程.
    轻量级锁(自旋锁):一个或多个线程通过CAS去争抢锁,如果抢不到则一直自旋.
    重量级锁:多个线程争抢锁,向内核申请锁资源,将未争抢成功的锁放到队列中直接阻塞.

    为什么要有锁的升级过程?
          在最开始的时候,其实就是无锁直接到重量级锁,但是重量级锁需要向内核申请额外的锁资源,这就涉及到用户态和内核态的转换,比较浪费资源,而且大多数情况下,其实还是一个线程去争抢锁,完全不需要重量级锁.

    锁的具体升级过程(通常情况下):

    1. 当只有一个线程去争抢锁的时候,会先使用偏向锁,就是给一个标识,说明现在这个锁被线程a占有.
    2. 后来又来了线程b,线程c,说凭什么你占有锁,需要公平的竞争,于是将标识去掉,也就是撤销偏向锁,升级为轻量级锁,三个线程通过CAS进行锁的争抢(其实这个抢锁过程还是偏向于原来的持有偏向锁的线程).
    3. 现在线程a占有了锁,线程b,线程c一直在循环尝试获取锁,后来又来了十个线程,一直在自旋,那这样等着也是干耗费CPU资源,所以就将锁升级为重量级锁,向内核申请资源,直接将等待的线程进行阻塞.

    锁升级的过程如下所示:

    在这里插入图片描述

    什么情况下偏向锁才会升级为轻量级锁,什么时候轻量级锁才会升级为重量级锁?

    1. 只有一个线程的时候就是偏向锁(当偏向锁开启的时候,偏向锁默认开启),当争抢的线程超过一个,升级为轻量级锁.
    2. 当自旋的线程循环超过10次,或者线程等待的数量超过cpu的1/2,升级为重量级锁.其实轻量级锁就适用于那种执行任务很短的线程,可能通过一两次自旋,就能够获取到锁.

    开启偏向锁一定比轻量级锁高效吗?
          不一定,比如在一开始已经知道某个资源就需要被多个线程争抢,此时就不需要开启偏向锁,因为偏向锁给了标识之后,还需要取消这个标识,重新抢锁,比如在JVM中,偏向锁默认是延迟4秒才开始的,因为JVM在启动的时候需要多个线程竞争资源,并且这个都是一开始知道的.

    二.对象在内存中的内存布局

    在堆中,一个对象会包含以下四个部分:
    在这里插入图片描述

    我们如果使用synchronized对某个对象进行加锁,就会体现在mark word区域.在最低两个字节加以标识.

    如下如所示:
    重量级锁 对应 10
    轻量级锁 对应 00
    无锁和偏向锁都对应01,这个时候需要倒数第三个字节加以区分,
    即 无锁 对应 001, 偏向锁 对应 101

    在这里插入图片描述

    二.实操

    在之前大概讲述了锁升级的过程和一些基础理论,现在用实际代码演示一下锁的升级过程.

    1.首先这里引入一个api,通过这个可以显示出对象的内存的信息.
    在这里插入图片描述
    演示:
    定义一个简单类,这个类有一个成员变量test.然后我们输出这个对象的内存信息.
    在这里插入图片描述
    输出信息如下:
    在这里插入图片描述
    2.使用程序演示锁升级过程
    如下,给定一个锁对象,然后依次用一个线程,两个线程,多个线程去争抢锁,演示锁升级过程.

    public class DemoApplication {
        static Object obj = new Object();
        //指定任务
         static class LockThread implements Runnable{
            @Override
            public void run() {
                synchronized (obj){
                    //进行一些耗时操作
                    String test="";
                    for (int i = 0; i < 1000; i++) {
                        test += "2";
                    }
                }
            }
        }
        public static void main(String[] args) {
    
            //只有一个线程去抢占锁
            new Thread(new LockThread()).start();
            System.out.println("===========偏向锁===========");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    
            //两个线程抢占锁,升级为轻量级锁
            new Thread(new LockThread()).start();
            System.out.println("===========轻量级锁===========");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    
            //多个线程抢占,一直在自旋,升级位重量级锁
            for (int i = 0; i < 20; i++) {
                new Thread(new LockThread()).start();
            }
            System.out.println("===========重量级锁===========");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    
        }
    }
    

    输出结果如下,可以很直观的看到偏向锁–>轻量级锁–>重量级锁
    在这里插入图片描述

    其他注意点:
          并不是一定会有一个偏向锁->轻量级锁->重量级锁的过程,比如如果出现严重的耗时操作(sleep,或者wait等),就会直接由偏向锁升级为重量级锁.

    展开全文
  • Synchronized锁升级过程

    千次阅读 2021-08-02 01:02:04
    一、Synchronized的锁升级过程 高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术, 包括偏向锁( Biased Locking )、轻量级锁( Lightweight ...

    一、Synchronized的锁升级过程

    高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,

    包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等等,

    这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

    无锁--》偏向锁--》轻量级锁–》重量级锁

     二、Java对象的布局

    术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.htmlJava对象的布局

    在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示: 

      

      

    2.1 对象头 

    当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?

    是存在锁对象的对象头中的。

    HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头

    arrayOopDesc对象用来描述数组类型。

    instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,

    另外,arrayOopDesc的定义对应 arrayOop.hpp 。 

     

    从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义在Hotspot源码中的 oop.hpp 文件中。

     

     

    在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata

    _mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark Word,它记录了对象和锁有关的信息

    _metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中,Klass表示普通指针、 _compressed_klass 表示压缩类指针

    对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。 

    2.2 Mark Word

    Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,

    占用内存大小与虚拟机位长一致。Mark Word对应的类型是markOop 。源码位于markOop.hpp 中。 

     

     

    在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下: 

    在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:

     2.3 klass pointer

    这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。

    该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

    如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。

    为了节约内存可以使用选项-XX:+UseCompressedOops 开启指针压缩,其中,oop即ordinary object pointer普通对象指针。

    开启该选项后,下列指针将压缩至32位:

    1. 每个Class的属性指针(即静态变量)

    2. 每个对象的属性指针(即对象变量)

    3. 普通对象数组的每个元素指针

    当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,

    比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

    对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)

    在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;

    在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;

    在64位系统中,Mark Word = 8 bytes,类型指针 = 4bytes,对象头 = 16 bytes = 96bits;开启指针压缩

    2.4 实例变量

    就是类中定义的成员变量。

    2.5 对齐填充

    对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,

    换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

    2.6 查看Java对象布局

            <dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.9</version>
            </dependency>

     

    三、偏向锁

    3.1 什么是偏向锁

    偏向锁是JDK 6中的重要引进,因为HotSpot作者经过研究实践发现,

    在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁

    偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID

    以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

    不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

     

     3.2 偏向锁原理

    当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

    1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。

    2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,

    如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

    持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

    3.3 偏向锁的撤销

    1. 偏向锁的撤销动作必须等待全局安全点
    2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
    3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

    偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟(4s)之后才激活,可以使用-XX:BiasedLockingStartupDelay=0 参数关闭延迟,

    如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过XX:-UseBiasedLocking=false 参数关闭偏向锁。

      

     3.4 偏向锁好处

    偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况

    偏向锁可以提高带有同步但无竞争的程序性能

    它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,

    如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

    在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。

    但在应用程序启动几秒钟之后才激活,可以使用-XX:BiasedLockingStartupDelay=0 参数关闭延迟,

    如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过XX:-UseBiasedLocking=false 参数关闭偏向锁。

    3.5 总结

    3.5.1 偏向锁的原理是什么?

    当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。

    同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,

    如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

    3.5.2 偏向锁的好处是什么?

    偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。

    偏向锁可以提高带有同步但无竞争的程序性能。

    四、轻量级锁

    4.1 什么是轻量级锁

    轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。

    首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。

    引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁

    所以轻量级锁的出现并非是要替代重量级锁

    4.2 轻量级锁原理

    当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下: 获取锁

    1. 判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,

    用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),

    将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Reocrd中的owner指向当前对象。

    2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。

    3. 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;

    否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

     

     

     4.3 轻量级锁的释放

    轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

    1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据。

    2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功。

    3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。

    对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,

    还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

     4.4 轻量级锁的好处

    在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

    4.5 总结

    轻量级锁的原理是什么?

    将对象的Mark Word复制到栈帧中的Lock Recod中。Mark Word更新为指向Lock Record的指针。

    轻量级锁好处是什么?

    在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

    五、自旋锁

    5.1 自旋锁原理

    synchronized (Demo01.class) {
        ...
        System.out.println("aaa");
    }

    前面我们讨论monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从用户态转为内核态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作

    这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,

    为了这段时间阻塞和唤醒线程并不值得

    如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间

    看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋) , 这项技术就是所谓的自旋锁。

    自旋锁在JDK 1.4.2中就已经引入 ,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 6中 就已经改为默认开启了。

    自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,

    因此,如果锁被占用的时间很短,自旋等待的效果就会非常好

    反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费

    因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。

    自旋次数的默认值是10次,用户可以使用参数-XX : PreBlockSpin来更改

    5.2 适应性自旋锁

    在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

    如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,

    进而它将允许自旋等待持续相对更长的时间,比如100次循环

    另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源

    有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。

     

     

     5.3 锁消除

    锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

    锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,

    认为它们是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,

    怎么会在明知道不存在数据争用的情况下要求同步呢?

    实际上有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。

    下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。

    StringBuffer的append ( ) 是一个同步方法,锁就是this也就是(new StringBuilder())。

    虚拟机发现它的动态作用域被限制在concatString( )方法内部。也就是说, new StringBuilder()对象的引用永远不会“逃逸”到concatString ( )方法之外,

    其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

    5.4 锁粗化

    原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,

    如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

    大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,

    那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗

    public class Demo08_LockCoarsing {
        public static void main(String[] args) {
            StringBuffer sb = new StringBuffer();
    
            for (int i = 0; i < 100; i++) {
                sb.append("a");
            }
    
            System.out.println(sb.toString());
        }
    }
    

    什么是锁粗化?

    JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。

    5.5 平时写代码如何对synchronized优化

    5.5.1 减少synchronize的范围

    同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

    synchronized (Demo01.class) {
        System.out.println("aaa");
    }

    5.5.2 降低synchronized锁的粒度

    将一个锁拆分为多个锁提高并发度

    Hashtable hs = new Hashtable();
    hs.put("aa", "bb");
    hs.put("xx", "yy");

     

     

     5.5.3 读写分离

    读取时不加锁,写入和删除时加锁

    ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

    视频教程参考博客

    展开全文
  • synchronized 锁升级过程

    2021-10-28 22:29:14
    synchronized 锁升级过程就是其优化的核心:偏向锁 -> 轻量级锁 -> 重量级锁 class Test{ private static final Object object = new Object(); public void test(){ synchronized(object) { // do ...

    synchronized 锁升级过程就是其优化的核心:偏向锁 -> 轻量级锁 -> 重量级锁

    class Test{
        private static final Object object = new Object(); 
        
        public void test(){
            synchronized(object) {
                // do something        
            }   
        }
        
    }
    

    每个对象创建时都有各自的对象头,用来记录持有锁的状态(如下图所示32位JVM下)

    一个对象创建时:
    1.如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即后 3 位为 101,这时它的 thread、epoch、age 都为 0。

    2.偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 XX:BiasedLockingStartupDelay=0 来禁用延迟。

    3.如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值。

    img

    无锁------------>偏向锁

    使用 synchronized 关键字锁住某个代码块的时候,当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。

    当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。如果从头到尾都是一个线程在使用锁,很明显偏向锁几乎没有额外开销,性能极高。

    在偏向锁状态下如果调用了对象的 hashCode,但此时偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被 撤销,而轻量级锁会在锁记录中记录 hashCode 重量级锁会在 Monitor 中记录 hashCode,所以无影响。

    偏向锁---------->轻量级锁

    一旦有第二个线程加入锁竞争,偏向锁转换为轻量级锁自旋锁)。锁竞争:如果多个线程轮流获取一个锁,但是每次获取的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程获取锁的时候,发现锁已经被占用,需要等待其释放,则说明发生了锁竞争。

    在轻量级锁状态上继续锁竞争,没有抢到锁的线程进行自旋操作,即在一个循环中不停判断是否可以获取锁。获取锁的操作,就是通过 CAS 操作修改对象头里的锁标志位。先比较当前锁标志位是否为释放状态,如果是,将其设置为锁定状态,比较并设置是原子性操作,这个 是 JVM 层面保证的。当前线程就算持有了锁,然后线程将当前锁的持有者信息改为自己。

    假如我们获取到锁的线程操作时间很长,比如会进行复杂的计算,数据量很大的网络传输等;那么其它等待锁的线程就会进入长时间的自旋操作,这个过程是非常耗资源的。其实这时候相当于只有一个线程在有效地工作,其它的线程什么都干不了,在白白地消耗 CPU,这种现象叫做忙等 (busy-waiting)。所以如果多个线程使用独占锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么 synchronized 就是轻量级锁,允许短时间的忙等现象。这是一种择中的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销

    每个线程都的栈帧都会包含一个锁记录(Lock Record)的结构,内部可以存储锁定对象的 Mark Word,让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录

    img

    如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

    img

    如果 cas 失败,有两种情况

    如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,会先进入自旋锁状态,最后进入重量级锁状态

    如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

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

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

    成功,则解锁成功

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

    轻量级锁--------->重量级锁

    显然,忙等是有限度的(JVM 有一个计数器记录自旋次数,默认允许循环 10 次,可以修改)。如果锁竞争情况严重, 达到某个最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是通过 CAS 修改锁标志位,但不修改持有锁的线程 ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是上面说的忙等,即不会自旋),等待释放锁的线程去唤醒。在 JDK1.6 之前, synchronized 直接加重量级锁,很明显现在通过一系列的优化过后,性能明显得到了提升。

    JVM 中,synchronized 锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有把这个称为锁膨胀的过程),不允许降级。

    那具体如何升级呢

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

    img

    这时 Thread-1 加轻量级锁失败,进入锁膨胀流程(这里我们就先忽略锁自旋)

    即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址

    然后自己进入 Monitor 的 EntryList BLOCKED
    img

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

    Monitor 被翻译为监视器或管程

    每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

    img

    刚开始 Monitor 中 Owner 为 null

    当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner

    在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED

    Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

    图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程

    展开全文
  • Synchronized详细介绍之锁升级过程

    千次阅读 2020-09-24 15:07:15
    Synchronized详细介绍之锁升级过程前言线程与进程的区别进程线程区别协程二级目录三级目录 前言 我们在并发编程过程中,会有一些资源或者操作是必须要进行序列化访问的,就是线程之间不能并发的访问,必须要进行串行...
  • 当前类的 Class 对象4.synchronized 底层原理1.Monitor 管程对象5.synchronized 优化升级过程   多线程编程中,会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源...
  • java锁升级过程

    万次阅读 多人点赞 2020-03-14 20:32:17
    java中对象有4种状态:(级别从低到高) 1.无锁状态 2.偏向状态 3.轻量级状态 4.重量级状态 对象头分两部分信息,第一部分用于存储哈希码、GC分代年龄等,这部分数据被称为"Mark Word"。在32位的HotSpot...
  • 答:在java6以前synchronized锁实现都是重量级锁的形式,效率低下,为了提升效率进行了优化,所以出现了锁升级过程。 问:我们通常说synchronized锁是重量级锁,那么为什么叫他重量级锁? 答:因为synchronized...
  • 深入理解synchronized锁升级过程

    千次阅读 多人点赞 2020-08-02 23:34:41
    1.简介 在Java高并发系统中,我们常常需要使用多线程... 修饰类方法(static修饰的方法):以类对象为,进入同步代码块前需要获得当前类对象的 修饰代码块:需要指定一个对象(既可以是实例对象,也可以是) 即
  • 的前置知识 如果想要透彻的理解java的来龙去脉,需要先了解锁的基础知识:的类型、java线程阻塞的代价、Markword。 的类型 从宏观上分类,分为悲观与乐观。 乐观 乐观是一种乐观思想,即认为读多...
  • java synchronized锁升级过程

    万次阅读 2020-06-10 17:38:31
    锁升级过程是怎样的呢? 主要一开始是无锁的状态,如果偏向锁未启动,当有其他线程竞争时,会升级为轻量级锁,如果偏向锁启动,则会升级为偏向锁,如果竞争激烈则会从偏向锁升级为重量级锁,如果不激烈则有偏向...
  • 在JDK1.6前,synchronized只是一把重量级的锁,而jdk1.6后,实现了偏向锁,轻量锁等,引入锁升级的机制,使得synchronized更加高效,性能更好,现在就来讲讲synchronized升级的过程。 对象头 首先我们要先知道什么是...
  • Java synchronized锁升级过程简述(面试可用)

    千次阅读 热门讨论 2021-04-30 09:45:53
    java 锁升级流程 Synchronized 的锁升级流程是这样:无锁 ----> 偏向锁 ----> 轻量级 锁 ----> 锁自旋 ----> 重量级锁 偏向锁 偏向锁,简单的讲,就是在锁对象的对象头中有个ThreaddId字段,这个...
  • 三分钟掌握synchronized锁升级过程

    千次阅读 2020-07-31 19:12:05
    三分钟掌握synchronized锁升级过程
  • synchronized锁升级过程详解

    千次阅读 2020-06-26 22:17:54
    第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。 第三步,两个线程都把对象的HashCode...
  • Java对象的锁升级过程 在JDK1.7版本之后,对Sunchronized关键字进行了优化,如果对对象进行Synchronized进行加锁,那么针对资源的竞争情况不同,锁会有一个升级的过程,以保证资源是最佳的利用状况。 过程可以概括为...
  • 2.锁升级 synchronized锁有四种状态,无锁,偏向锁,轻量级锁,重量级锁,这几个状态会随着竞争状态逐渐升级,锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态 「无锁 ==> 偏向锁 ==> 轻量级锁 ==&...
  • synchronized锁升级过程和实现原理

    千次阅读 2020-07-04 18:42:25
    整个锁升级过程大概分为: new-->偏向锁-->轻量级锁(无锁,自旋锁,自适应自旋)-->重量级锁 我们结合下面这张图具体谈一下 从刚刚new出来的时候,首先上的是偏向锁,出现争用,升级为轻量级锁,竞争...
  • Synchronized四种锁状态 在 Java 语言中,使用 Synchronized 是能够实现线程同步的,即加锁。并且实现的是悲观锁,在操作同步...要想清晰地了解锁升级过程,首先需要我们掌握内存布局,很多公司会问到这样一个问题: O
  • synchronized实现原理及锁升级过程

    万次阅读 2019-08-08 13:50:10
    synchronized实现原理及锁升级过程 前言: synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的 尽管最初synchronized的性能效率比较差,但是随着版本的升级,synchronized已经变得原来...
  •  synchronized实现同步的基础:java中的每一个对象都可以作为(表现为3种形式)。 对于普通同步方法,是当前实例对象(this指针) 对于静态同步方法,是当前类的Class对象(xxx.class) 对于同步方法块,...
  • Java锁升级过程

    千次阅读 2019-10-12 10:31:14
    因为经过HotSpot的作者大量的研究发现,大多数时候是不存在竞争的,常常是一个线程多次获得同一个,因此如果每次都要竞争会增大很多没有必要付出的代价,为了降低获取的代价,才引入的偏向。 轻量级 ...
  • 2.轻量级和重量级升级 3. 消除 像 StringBuffer 在方法体内部的话,因为虚拟机栈是线程所有的,方法对应虚拟机栈中的栈帧,是线程安全的,会JVM会将当前的进行消除处理,处于无锁的状态 4.Monitor ...
  • synchronized升级过程详解

    千次阅读 2021-01-22 17:14:20
    在 Java 早期版本中,synchronized属于重量级,效率低下。为什么呢?因为监视器(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果 要挂起或者唤醒一...
  • java锁升级过程

    千次阅读 2021-10-05 16:30:24
    一、锁升级 为什么要引入偏向锁? 因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 90,459
精华内容 36,183
关键字:

锁升级过程

友情链接: Fresh-Content.rar