JVM内置锁synchronized关键字

提问环节:

设计同步器的意义

如何解决线程并发安全问题

synchronized原理

Monitor监视器锁

对象的内存布局、对象头

对象头分析工具

锁的膨胀升级过程

偏向锁

轻量级锁

自旋锁

锁消除

逃逸分析

设计同步器的意义

多线程编程中,有可能出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称为临界资源;这种资源可能是:对象、变量、文件等。

由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。

如何解决线程并发安全问题?

通过同步互斥访问,即在同一时刻,只能有一个线程访问临界资源。

java提供了两种方式实现同步互斥访问:synchronized和Lock

同步器的本质就是加锁

加锁的目的,序列化访问临界资源。

注意:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。

synchronized原理

synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。

加锁的方式:

1、同步实例方法,锁是当前实例对象

2、同步类方法,锁是当前类对象

3、同步代码块,锁是括号里面的对象

    synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁,性能较低。JVM内置锁在1.5之后版本做了重大的优化,如锁粗化、锁消除、轻量级锁、偏向锁、适应性自旋等技术来减少锁操作的开销,内置锁的并发性能已基本与Lock持平。

    synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令分别在同步块逻辑代码的起始位置与结束位置。

 每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下:

Monitor监视器锁

        任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽具体实现细节不一样,但都可以通过成对的MonitorEnter和MonitorExit指令来实现。

        monitorenter:每个对象都是一个监视器锁,当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

        a、如果monitor的进入数为0,则该线程进入monitor,然后设置为1,该线程即为monitor的所有者;

        b、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;

        c、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

        monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

通过上述描述,可以清楚的看出synchronized的实现原理,synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出异常java.lang.illegalMonitorStateException.

wait/notify方法属于被加锁的对象,此对象就是一个monitor监视器。

什么是monitor?

可以理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象天生是Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说的Synchronized对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其由C++实现的。

ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

        1、首先会进入_EntryList集合,当线程获取到对象的monitor后,进入_owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;

        2、若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒;

        3、当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

        同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。

监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问,锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识认识一下对象的内存布局

对象的内存布局

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding):

        对象头:Java对象头一般占用2个机器码(4or8个字节,根据虚拟机位数是32or64决定的),包含如hash码、对象所属年代、对象锁、锁状态标志、偏向锁(线程)ID、偏向时间、数组对象(额外需要一个字节码记录数组长度)等。

        实例数据:存放类的属性数据信息,包括父类的属性信息;

        对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数组仅为了了字节对齐,非必须存在。

 对象头

        HotSpot虚拟机的对象头包括两部分信息,第一部分是Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键

        考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息。

        现在虚拟机基本都是64位的,JVM默认会开启指针压缩,所以基本上也是按32位的形式记录对象头

        《java性能权威指南》提到当heap size堆内存大于32G,是用不了压缩指针的,对象引用会额外占用20%左右的堆空间,也就是意味着38G的内存才相当于开启了指针压缩的32G堆空间。

对象头分析工具

运行时对象头锁状态分析工具JOL,是OpenJDK开源工具包,maven依赖如下:

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

打印markword

System.out.println(ClassLayout.parseInstance(object).toPrintable());

锁的膨胀升级过程

        锁的状态共四种:无锁、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,锁的升级是单向的,不会出现锁的降级。

        偏向锁:Java6之后加入的新锁,为了减少同一线程获取锁的代价(CAS操作)而引入偏向锁。思想:如果一个线程获得了锁,那么锁就进入了偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需任何同步操作,即获得锁。

        轻量级锁:轻量级锁涉及到一个自旋的问题,而自旋操作是会消耗CPU资源的。为了避免无用的自旋,当锁资源存在线程竞争是,偏向锁就会升级为重量级锁来避免其他线程无用的自旋操作。轻量级锁的优点:自旋代替阻塞,避免了线程切换带来的时间消耗,提高了程序响应速度;缺点:如果始终无法获得锁资源,线程就会自旋消耗cpu资源。

        自旋锁:轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,进行一项称为自旋锁的优化手段。空循环(50-100)避免线程挂起到导致的切换线程,从用户态装换到核心态这一耗时的高成本操作。

        锁消除:Java虚拟机在JIT(即时编译)编辑时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,可以节省毫无意义的请求锁时间。如:StringBuffer的append是一个同步方法,但在add方法中的StringBuffer属于一个局部变量,并不会被其他线程所使用,因此不存在共享资源竞争的情况,JVM会自动将其锁消除。

        锁消除,前提是java必须运行在server模式,同时必须开启逃逸分析

-XX:+DoEscapeAnalysis        开启逃逸分析

-XX:+EliminateLocks        开启锁消除

运行java -version,即可知道该JVM是运行在Client模式还是Server模式

java -version

 64位jdk 1.8默认是Server模式

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值