synchronized原理&优化

一.加锁的底层原理

1.synchronized同步块中,Java虚拟机实现加锁的原理

比如某一个类
java文件编译成class文件的时候,虚拟机帮我们插入了一些东西
在这里插入图片描述
我们将class文件反编译一下,可以看到有序号①和序号②两处,即Java文件和class文件的对应。真正的+1操作是在红框里面进行的。而进挨着红框的两个monitorentermonitorexit就是synchronized的原型。也就是说对于synchronized虚拟机会缺省地帮我们在字节码文件中插入两个指令monitorentermonitorexit,来实现锁功能。
其实加锁的本质是线程拿到对monitor对象的所有权。谁拥有monitor对象的所有权谁就拥有锁,谁释放了对monitor对象的所有权,就是释放了锁

关于monitorentermonitorexit

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。每个monitorenter必须有对应的monitorexit与之配对。而且任何对象都有一个monitor与之关联。也就是这个对象锁。谁拥有monitor对象的所有权谁就拥有相应对象的锁,谁释放了对monitor对象的所有权,就是释放了锁

2.synchronized方法中,Java虚拟机实现加锁的原理

会在flags中出现不同。但是底层还是monitor,只是在字节码中无法体现。所以monitor就是锁底层的核心。
在这里插入图片描述

3.Lock底层原理是AQS,不是monitorentermonitorexit

二.synchronized优化

1.synchronized加在对象哪里

synchronized加在对象哪里,才能标识这个对象拥有锁了呢?
加在对象头里面,对象头是java虚拟机里面的内容,在此还无需深究。但是咱们只需要知道对象头包含此对象很多关键的信息,比如GC年龄,对象的hashCode等等,很多都是虚拟机进行操作的,而不是程序员进行操作的。对象头的另一个区域还保存着这个对象属于哪一个类(类型指针)也就是对象头有两类,前者被称为MarkWord,后者被称为KlassPoint对象的锁放在MarkWord里面。(如果是数组,还多一个对象头,保存数组长度)

2.synchronized的变化

synchronized并不是一上来就是以“重量级锁”的形式发挥功能的。它会经历多种变化,最后才是“重量级锁”的形式。而标志某个对象的锁到底是什么状态,是存放在对象头里面的。原来在对象头相同区域里面的东西可能会存放到属于这个对象的另外一块空间。也就是说对象头也是在不断发生变化的。
synchronized的变化是:无锁状态,偏向锁,轻量级锁,重量级锁。
这些状态就是为了提高synchronized性能而做的一些优化

3.轻量级锁

线程没有抢到锁,会进行两次上下文切换(挂起,唤醒),会非常耗费时间。所以就引入了偏向锁,轻量级锁等等这些方式,不让它搞得这么“重”。
轻量级锁的核心就是通过CAS操作来进行加锁和解锁。当线程A,B,C同时竞争一个锁时,比如线程A抢到了这个锁,那么按原来的思想,B和C就直接挂起了,会非常耗费时间,显得很“重”。但是线程A执行的操作可能很快就完了,那么B 和 C为啥不稍微等一下呢?也就是进行自旋,就是不断地尝试去获得锁。这个自旋的次数和时间在Java的发展过程中发生了一些变化,目前的自旋时间是进行上下文切换的时间(其实也可以理解,如果自旋时间都超过了上下文切换的时间,那我还要你自旋干啥,直接挂起不就得了)。自旋对CPU也是有消耗的,所以一定要有限制,不然得不偿失。
自旋锁是轻量级锁里面的概念,能控制自旋次数的自旋锁也被称为适应性自旋锁。次数由虚拟机自行控制调整。

4.偏向锁

经过大量的统计,发现一个锁总是由同一个线程获得。所以干脆连CAS操作我都不想做了,就测试一下(在对象头里面)当前拥有这把锁的是不是我自己,如果是我自己就直接来用。
所以偏向锁的含义是这个锁的拥有者总是偏向于第一次拥有这把锁的线程。具体就是说第一次将对象由无锁状态转变为偏向锁状态的时候,进行一次CAS操作,再往后运行的时候,只是检测一下这个锁的拥有者是不是自己,如果是的话,CAS操作直接不用做,直接用锁。
当然,上面说的是大多数的情况,也就是没有竞争的情况。当有竞争的时候,偏向锁则升级为轻量级锁,就是上面介绍的那个。那么如何升级为轻量级锁呢?就是通过stw操作。

stop the world(stw):停止一切

在进行偏向锁撤销的时候,必须要进行stw操作。为什么呢?
我们先解释下stw是什么意思?当没有stw操作的时候,我们可以想象一下,工作线程会不断地往堆里面产生对象,当堆满的时候,垃圾回收线程则进行回收。假如没有stw,则会出现这样一种情况:垃圾回收线程刚回收掉一个对象,你工作线程就给我扔了一个新的对象进去。对比到现实生活中,就是我刚扫完垃圾,你就给我扔了垃圾,这是不是很气人?所以为了解决这个问题,就引入了stw操作。当需要进行垃圾回收的时候,会为各个工作线程划一条“线”。当所有的工作线程进行到那个位置的时候,全部停止,此时垃圾回收线程开始专心地回收对象。当垃圾回收线程工作完的时候,会启动所有工作线程继续工作。
那么为什么撤销偏向锁会stw呢?其实很好理解。当线程1拥有偏向锁的时候,线程2此时要撤销线程1的偏向锁,其实就是更改线程1所拥有的堆栈当中的内容。如果线程1一直在运行,那么堆栈就一直在变化,就很难去撤销偏向锁的相应信息。所以要让线程1停止工作,线程2撤销掉线程1的偏向锁之后就让线程1继续工作

5.锁的状态

所以锁的四种状态的转换逻辑是。一开始是无锁状态,后来线程拥有锁之后,先是偏向锁状态,后来如果有其它线程与它竞争,则偏向锁膨胀为轻量级锁状态,一个线程在拥有锁的状态下进行操作,另一个线程进行自旋。当自旋超过一定次数和时间后,就膨胀为我们最熟悉的重量级锁状态,也就是最“重”的状态。这就是synchronized的底层的四种状态,也是synchronized的优化。
在这里插入图片描述

6.注意

偏向锁没有体现synchronized的可重入性,它只是synchronized的一个优化方向,是一个线程释放之后,再次获取。而不是没有释放就再次获取(这才是可重入性)。但是synchronized是可重入锁。
偏向锁就好比你去一个顾客始终只有你自己的酒吧,第一次去就要去前台点酒(CAS操作),此时酒喝不完,你就把酒存起来,存到酒吧,酒吧也知道这个酒是你的(相当于一直拥有了酒吧的对象),离开酒吧(释放锁,但是一直拥有酒吧的对象),当下一次去酒吧的时候,就可以直接拿酒,继续喝(可以判断酒吧对象的锁上一次是你自己锁住的,此时省去了CAS操作)。

三.总结

总结一下,就是如果线程要拥有锁,首先要得到此对象对应的monitor对象的拥有权,执行monitorenter指令和monitorexit指令。拥有锁后,线程由无锁状态转换为偏向锁状态,如果存在竞争则转换为轻量级锁状态,如果自旋时间过长则转换为重量级锁状态。
引入其他博客的一段,是:

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

扩展,对象头装的关于锁的东西(随便看看)

在这里插入图片描述

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值