一.加锁的底层原理
1.synchronized同步块中,Java虚拟机实现加锁的原理
比如某一个类
由java
文件编译成class
文件的时候,虚拟机帮我们插入了一些东西
我们将class
文件反编译一下,可以看到有序号①和序号②两处,即Java
文件和class
文件的对应。真正的+1
操作是在红框里面进行的。而进挨着红框的两个monitorenter
和monitorexit
就是synchronized
的原型。也就是说对于synchronized
,虚拟机会缺省地帮我们在字节码文件中插入两个指令monitorenter
和monitorexit
,来实现锁功能。
其实加锁的本质是线程拿到对monitor
对象的所有权。谁拥有monitor
对象的所有权谁就拥有锁,谁释放了对monitor
对象的所有权,就是释放了锁
关于monitorenter
和monitorexit
monitorenter
指令是在编译后插入到同步代码块的开始位置,而monitorexit
是插入到方法结束处和异常处。每个monitorenter
必须有对应的monitorexit
与之配对。而且任何对象都有一个monitor
与之关联。也就是这个对象锁。谁拥有monitor
对象的所有权谁就拥有相应对象的锁,谁释放了对monitor
对象的所有权,就是释放了锁
2.synchronized方法中,Java虚拟机实现加锁的原理
会在flags
中出现不同。但是底层还是monitor
,只是在字节码中无法体现。所以monitor
就是锁底层的核心。
3.Lock
底层原理是AQS
,不是monitorenter
和monitorexit
二.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以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。