榆树范文网

锁升级总结(精选8篇)

87

锁升级总结 第1篇

在早期的jdk版本中,synchronized是一个重量级锁,保证线程的安全但是效率很低。后来对synchronized进行了优化,有了一个锁升级的过程

无锁态(new)-->偏向锁-->轻量级锁(自旋锁)-->重量级锁

通过MarkWord中的8个字节也就是64位来记录锁信息。也有人将自旋锁称为无锁,因为自选操作并没有给一个对象上锁,这里只要理解意思即可。

当给一个对象增加synchronized锁之后,相当于上了一个偏向锁

当有一个线程去请求时,就把这个对象MarkWord的ID改为当前线程指针ID(JavaThread),只允许这一个线程去请求对象。

当有其他线程也去请求时,就把锁升级为轻量级锁。每个线程在自己的线程栈中生成LockRecord,用CAS自旋操作将请求对象MarkWordID改为自己的LockRecord,成功的线程请求到了该对象,未成功的对象继续自旋。

如果竞争加剧,当有线程自旋超过一定次数时(在之后,这个自旋次数由JVM自己控制),就将轻量级锁升级为重量级锁,线程挂起,进入等待队列,等待操作系统的调度。

synchronized关键字被编译成字节码之后会被翻译成monitorenter和monitorexit两条指令,进入同步代码块时执行monitorenter,同步代码块执行完毕后执行monitorexit

在某些情况下,如果JVM认为不需要锁,会自动消除锁,比如下面这段代码:

StringBuffer是线程安全的,但是在这个add方法中stringbuffer是不能共享的资源,因此xxx只会徒增性能消耗,JVM就会消除StringBuffer内部的锁。

在某些情况下,JVM检测到一连串的操作都在对同一个对象不断xxx,就会将这个锁加到这一连串操作的外部,比如:

上述操作StringBuffer每次添加数据都要xxx和解锁,连续100次,这时候JVM就会将锁加到更外层(while)部分。

首先问一个经常基础的虚拟机问题,实例对象存放在虚拟机的哪个位置?按以前的回答,示例对象放在堆上,引用放在栈上,示例的元数据等存放在方法区或者元空间。

但这是有前提的,前提是示例对象没有线程逃逸行为。

开始默认开启了逃逸分析,所谓逃逸分析,就是指如果一个对象被编译器发现只能被一个线程访问,那么这个对象就不需要考虑同步。JVM就对这种对象进行优化,将堆分配转化为栈分配,归根结底就是虚拟机在编译过程中对程序的一种优化行为。

锁升级总结 第2篇

其实这是一个妥协,明确知道在刚开始执行代码时,一定有好多线程来抢锁,如果开了偏向锁效率反而降低,所以上面程序在睡了5s之后偏向锁才开放。为什么加偏向锁效率会降低,因为中途多了几个额外的过程,上了偏向锁之后多个线程争抢共享资源的时候要进行锁升级到轻量级锁,这个过程还的把偏向锁进行撤销在进行升级,所以导致效率会降低。为什么是4s?这是一个统计的时间值。

当然我们是可以禁止偏向锁的,通过配置参数-XX:-UseBiasedLocking = false来禁用偏向锁。jdk15之后默认已经禁用了偏向锁。本文是在jdk8的环境下做的锁升级验证。

2 锁的升级流程

上面已经验证了对象从创建出来之后进内存从无锁状态->偏向锁(如果开启了)->轻量级锁的过程。对于锁升级的流程继续往下,轻量级锁之后就会变成重量级锁。首先我们先理解什么叫做轻量级锁,从一个线程抢占资源(偏向锁)到多线程抢占资源升级为轻量级锁,线程如果没那么多的话,其实这里就可以理解为CAS,也就是我们说的Compare and Swap,比较并交换值。在并发编程中最简单的一个例子就是并发包下面的原子操作类AtomicInteger。在进行类似++操作的时候,底层其实就是CAS锁。

锁升级总结 第3篇

1 锁的升级验证

探讨锁的升级之前,先做个实验。两份代码,不同之处在于一个中途让它睡了5秒,一个没睡。看看是否有区别。

这两份代码会不会有什么区别?运行之后看看结果:

有点意思的是,让主线程睡了5s之后输出的内存布局跟没睡的输出结果居然不一样。

Syn锁升级之后,版本的一个底层默认设置4s之后偏向锁开启。也就是说在4s内是没有开启偏向锁的,加了锁就直接升级为轻量级锁了。

那么这里就有几个问题了?

锁升级总结 第4篇

其实这本质上归根于一个概率问题,统计表示,在我们日常用的syn锁过程中70%-80%的情况下,一般都只有一个线程去拿锁,例如我们常使用的、StringBuffer,虽然底层加了syn锁,但是基本没有多线程竞争的情况。那么这种情况下,没有必要升级到轻量级锁级别了。偏向的意义在于:第一个线程拿到锁,将自己的线程信息标记在锁上,下次进来就不需要在拿去拿锁验证了。如果超过1个线程去抢锁,那么偏向锁就会撤销,升级为轻量级锁,其实我认为严格意义上来讲偏向锁并不算一把真正的锁,因为只有一个线程去访问共享资源的时候才会有偏向锁这个情况。

无意使用到锁的场景:

锁升级总结 第5篇

首先我们可以思考的是多个线程的时候先开启轻量级锁,如果它carry不了的情况下才会升级为重量级。那么什么情况下轻量级锁会carry不住。1、如果线程数太多,比如上来就是10000个,那么这里CAS要转多久才可能交换值,同时xxx光在这10000个活着的线程中来回切换中就耗费了巨大的资源,这种情况下自然就升级为重量级锁,直接叫给操作系统入队管理,那么就算10000个线程那也是处理休眠的情况等待排队唤醒。2、CAS如果自旋10次依然没有获取到锁,那么也会升级为重量级。

总的来说2种情况会从轻量级升级为重量级,10次自旋或等待cpu调度的线程数超过cpu核数的一半,自动升级为重量级锁。看服务器xxx的核数怎么看,输入top指令,然后按1就可以看到。

锁升级总结 第6篇

上面我们对对象的内存布局有了一些了解之后,知道锁的状态主要存放在markword里面。这里我们看看底层实现。

对这段简单代码进行反解析看看什么情况。javap -c

首先我们能确定的是syn肯定是还有xxx的操作,看到的信息中出现了monitorenter和monitorexit,主观上就可以猜到这是跟xxx和解锁相关的指令。有意思的是1个monitorenter和2个monitorexit。为什么呢?正常来说应该就是一个xxx和一个释放锁啊。其实这里也体现了syn和lock的区别。syn是JVM层面的锁,如果异常了不用自己释放,jvm会自动帮助释放,这一步就取决于多出来的那个monitorexit。而lock异常需要我们手动补获并释放的。

关于这两条指令的作用,我们直接参考JVM规范中描述:

翻译一下:

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

翻译一下:

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

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

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加i。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线xxx该锁。当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

总结

以往的经验中,只要用到synchronized就以为它已经成为了重量级锁。在之前确实如此,后来发现太重了,消耗了太多操作系统资源,所以对synchronized进行了优化。以后可以直接用,至于锁的力度如何,JVM底层已经做好了我们直接用就行。

最后再看看开头的几个问题,是不是都理解了呢。带着问题去研究,往往会更加清晰。希望对大家有所帮助。

锁升级总结 第7篇

对象内存存在一把锁,锁信息放在了对象头的Mark Word当中。在最后两位中,代表了锁标志位:无锁、偏向锁、轻量级锁、重量级锁。

synchronized关键字可以用来同步线程,synchronized被编译后,会生成monitorenter,monitorexit两个字节码指令。JVM以此来进行线程同步。

monitor即为同步监视器,一个线程进入了monitor,其他线程只能够等待,当且只有这个线程退出,其他线程才有机会竞争到所资源进行执行。

Entry Set中聚集了一些想要进入Monitor的线程,它们处于waiting状态,假设某个名为A线程成功进入了Monitor,那么它就处于active状态。假设此时A线程执行途中,遇到一个判断条件,需要它暂时让出执行权,那么它将进入wait set,状态也被标记为waiting。这时entry set中的线程就有机会进入monitor,假设一个线程B成功进入并且顺利完成,那么它可以通过notify的形式来唤醒wait set中的线程A,让线程A再次进入Monitor,执行完成后便退出。

这就是synchronized的同步机制,但是synchronized可能存在性能问题,因为monitor是依赖于操作系统的Mutex Lock来实现的,Java线程事实上是对操作系统线程的映射,所以每当挂起或唤醒一个线程都要切换到操作系统的内核态,这个操作是比较重量级的。在一些情况下,甚至切换时间本身就会超出线程执行任务的时间,这样的话,使用synchronized将会对程序的性能产生影响。

从Java6开始,synchronized关键字就进行了优化,引入了“偏向锁”,“轻量级锁”,所以锁共有四种状态,分别为:无锁、偏向锁、轻量级锁、重量级锁。

锁升级总结 第8篇

无意识中用到锁的情况:

简单xxx发生了什么?

要弄清楚xxx之后到底发生了什么需要看一下对象创建之后再内存中的布局是个什么样的?

一个对象在new出来之后在内存中主要分为4个部分:

知道了这4个部分之后,我们来验证一下底层。借助于第三方包 JOL = Java Object Layout java内存布局去看看。很简单的几行代码就可以看到内存布局的样式:

将结果打印出来:

从输出结果看:

1)对象头包含了12个字节分为3行,其中前2行其实就是markword,第三行就是klass指针。值得注意的是在xxx前后输出从001变成了000。Markword用处:8字节(64bit)的头记录一些信息,锁就是修改了markword的内容8字节(64bit)的头记录一些信息,锁就是修改了markword的内容字节(64bit)的头记录一些信息。从001无锁状态,变成了00轻量级锁状态。

2)New出一个object对象,占用16个字节。对象头占用12字节,由于Object中没有额外的变量,所以instance = 0,考虑要对象内存大小要被8字节整除,那么padding=4,最后new Object() 内存大小为16字节。

拓展:什么样的对象会进入老年代?很多场景例如对象太大了可以直接进入,但是这里想探讨的是为什么从Young GC的对象最多经历15次Young GC还存活就会进入Old区(年龄是可以调的,默认是15)。上图中hotspots的markword的图中,用了4个bit去表示分代年龄,那么能表示的最大范围就是0-15。所以这也就是为什么设置新生代的年龄不能超过15,工作中可以通过-XX:MaxTenuringThreshold去调整,但是一般我们不会动。