当前位置: 首页 > 后端技术 > Java

JAVA并发编程——同步与锁升级

时间:2023-04-01 18:12:16 Java

1.同步性能变化2.同步锁类型及升级步骤3.JIT编译器对锁的优化4.总结1.同步性能变化我们都知道synchronized关键字,它可以让程序串行执行,保证数据的安全性,但性能会下降。因此java对synchronized做了一系列的优化:在java5之前:synchronized只是synchronized,这个操作是一个重量级的操作,cpu进入锁定程序后,会在用户态和内核态之间切换。用户态:用户态运行较低级别的用户程序。内核模式:内核模式运行操作系统程序并操作更高级别的硬件。java要阻塞或者唤醒一个线程,需要操作系统的访问,需要在用户态和核心态之间切换,因为synchronized是重量级锁,需要依赖于底层操作系统的互斥锁,挂起线程和恢复线程需要进入内核态才能完成。这种切换会消耗大量的系统资源。如果同步代码块中的内容过于简单,切换时间可能比用户代码的执行时间还长,时间成本过高。这就是为什么同步早起效率低下的原因。Java6开始:OptimizeSynchronized。为了减少获取和释放锁带来的性能消耗,引入了偏向锁、轻量级锁和重量级锁(减少线程阻塞和唤醒)。2.同步锁类型及升级步骤在谈同步锁升级之前,我们首先要了解线程访问一个同步修改的方法有以下三种:1)只有一个线程可以访问,而且是唯一的。2)有两个线程A和B交替访问3)竞争激烈,多线程访问。还记得我们上一篇博客JAVA并发编程——Java对象内存布局与对象头提到了对象头,我们先来看这张图:我们可以看到synchronized使用的锁是存放在java对象头的MarkWord中的。锁升级功能主要依赖MarkWord中的锁标志和偏向释放的锁标志。Java的锁升级按照无锁->偏向锁->轻量级锁->重量级锁一一讲解。无锁:先看一段无锁代码publicstaticvoidmain(String[]args){//-XX:-UseCompressedClassPointers-XX:BiasedLockingStartupDelay=0Objecto=newObject();System.out.println(ClassLayout.parseInstance(o).toPrintable());}这个对象没有使用锁,我们看一下运行结果。这样输出的结果是objectheader是倒着输出的,红色标记的地方是lockflag,现在是001,对应的objectheader是lock-free状态。偏向锁:当一段同步代码被同一个线程多次访问时,由于只有一个线程,该线程后续访问时会自动获取锁。(这个锁偏向于经常访问的线程。)在测试偏向锁之前,记得输入jvm参数:-XX:+UseBiasedLocking-XX:BiasedLockingStartupDelay=0因为jdk1.6之后默认开启了偏向锁,但是startup是有延时的,所以需要手动添加参数,让偏向锁的延时时间为0,程序一启动就马上开启。对象o=新对象();newThread(()->{synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}},"t1").start();运行结果为:轻量级锁:上面刚才说的是只有一个线程竞争一个资源类,但是现在当有其他线程逐级竞争锁的时候,偏向锁就不能用了,需要升级到一个轻量级的锁。锁。当第一个线程在执行synchronized方法时(在同步块中),还没执行完,其他线程过来抢,竞争线程使用cas更新对象头失败,偏向锁就会被取消,将发生锁升级。此时轻量级锁被原来持有偏向锁的线程持有,继续执行自己的同步代码,而竞争线程会进入自旋等待获取轻量级锁。//关闭延迟参数并启用此功能Objecto=newObject();newThread(()->{synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}},"t1").start();那么比赛旋转多少次才能升级锁呢?java6之前:默认自旋次数为10次:-XX:PreBlockSpin=10或者自旋次数超过cpu核数的一半java6之后:Adaptive:表示自旋次数不固定。它基于:同一锁的最后自旋时间和拥有该锁的线程的状态来确定。偏向锁和自旋锁的区别:当轻量级锁竞争失败时,自旋会尝试抢占锁。轻量级锁每次退出一个同步代码块都需要释放锁,而偏向锁只有在竞争发生时才会释放锁。重度锁:大量线程在抢占同一个资源类,冲突高,会升级为重量级锁。newThread(()->{synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}},"t1").start();newThread(()->{synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}},"t2").start();newThread(()->{synchronized(o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}},"t3").start();3、JIT编译器对锁的优化1)锁消除当只有一个线程运行synchronized代码时,默认会消除锁以节省资源。/***LockElimination*从JIT的角度来说,相当于忽略它,synchronized(o)不存在,这个锁对象还没有被共享传播给其他线程,*极端的说,没有底这个锁对象在所有机器代码层,消除锁的使用*/publicclassLockClearUPDemo{staticObjectobjectLock=newObject();//正常publicvoidm1(){//锁消除,JIT会忽略它,synchronized(对象锁)不存在了。不健康对象o=newObject();synchronized(o){System.out.println("-----helloLockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode());}}publicstaticvoidmain(String[]args){LockClearUPDemodemo=newLockClearUPDemo();for(inti=1;i10;i++){newThread(()->{demo.m1();},String.valueOf(i)).start();}}}2)锁粗化在同一个方法中加一个锁,头尾相连,前后都是一个锁对象,那么编译器会把这些synchronized合并成一个大块,加粗范围,节省资源./***锁粗化*如果方法首尾相接,前后都是同一个锁对象,那么JIT编译器会将这些同步块合并成一个大块,*粗体增加作用域,只适用for一次锁,避免反复申请和释放锁,提高性能*/publicclassLockBigDemo{staticObjectobjectLock=newObject();publicstaticvoidmain(String[]args){newThread(()->{同步(objectLock){System.out.println("11111");}同步(objectLock){System.out.println("22222");}同步(objectLock){System.out.println("33333");}},"a").start();newThread(()->{synchronized(objectLock){System.out.println("44444");}synchronized(objectLock){System.out.println("55555");}synchronized(objectLock){System.out.println("66666");}},"b").start();}}4.总结锁升级的过程图片锁优缺点适用场景偏向于锁。加锁和解锁不需要额外的消耗,执行异步方法之间只有纳秒级的差距。如果线程间存在锁竞争,会带来锁取消的额外消耗。只有一个线程访问同步块的场景是轻量级的。有锁竞争的线程不会被阻塞,线程相应的速度会提高。如果CPU还是不可用,就会空闲,浪费CPU去追寻相应的时间,执行同步块非常快的重量级锁线程竞争不申请自旋,不消耗cpu线程阻塞,导致用户态和内核态切换,响应时间慢,追求数据一致性,同步执行块执行速度长锁升级可以总结一句话:先自旋,失败再阻塞,就是把之前的悲观锁(重量级锁)改成偏向锁和一定条件下的轻量级锁。Synchronized在字节码中的修改方式和代码块实现有很大不同,但内部实现仍然是基于对象头的MarkWord。在JDK1.6之前,synchronized使用的是重量级锁。JDK1.6之后优化了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是不管什么情况都使用重量级锁。

猜你喜欢