一、Synchronized实现原理
1、Synchronized锁的3中形式
利用 synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的 Class 对象。
对于同步方法块,锁是 Synchonized 括号里配置的对象。
2、Synchronized在JVM里的实现
从 JVM 规范中可以看到 Synchonized 在 JVM 里的实现原理,JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的,细节在 JVM 规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。
monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处,JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
3、Java对象头
synchronized 用的锁是存在 Java 对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即 32bit,如表1所示。
表1 Java 对象头的长度
Java 对象头里的 Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。32位 JVM 的 Mark Word 的默认存储结构如表2所示。
表2 Java 对象头的存储结构
在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为存储以下4种数据,如表3所示。
表3 Mark Word 的状态变化
在64位虚拟机下,Mark Word 是 64bit 大小的,其存储结构如表2-5所示。
表4 Mark Word 的存储结构
二、锁升级
1、锁的4种状态
无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)。
2、偏向锁
为什么要引入偏向锁?
HotSpot 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁的升级
当线程 1 访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,因为偏向锁不会主动释放锁,因此以后线程 1 再次获取锁的时候,需要比较当前线程的线程 ID 和对象头中的线程 ID 是否一致,如果一致(还是线程 1 获取锁对象),则无需使用 CAS 来加锁、解锁;如果不一致(其他线程,如线程 2 要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程 1 的线程 ID),那么需要查看对象头中记录的线程 1 是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程 2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程 1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程 1,撤销偏向锁,升级为轻量级锁,如果线程 1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
3、轻量级锁
为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要 CPU 从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋着等待锁释放。
轻量级锁什么时候升级为重量级锁?
线程 1 获取轻量级锁时会先把锁对象的对象头 Mark Word 复制一份到线程 1 的栈帧中创建的用于存储锁记录的空间(称为 Displaced Mark Word),然后使用 CAS 把对象头中的内容替换为线程 1 的锁记录地址;
如果在线程 1 复制对象头的同时(在线程 1 CAS 之前),线程 2 也准备获取锁,复制了对象头到线程 2 的锁记录空间中,但是在线程 2 CAS 的时候,发现线程 1 已经把对象头换了,线程 2 的 CAS 失败,那么线程 2 就尝试使用自旋锁来等待线程 1 释放锁。
但是如果自旋的时间太长也不行,因为自旋是要消耗 CPU 的,因此自旋的次数是有限制的,比如 10 次或者 100 次,如果自旋次数到了线程 1 还没有释放锁,或者线程 1 还在执行,线程2还在自旋等待,这时又有一个线程 3 过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止 CPU 空转。
注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
4、这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)
总结
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用 CAS 操作,并将对象头中的线程 ID 改成自己的 ID,之后再次访问这个对象时,只需要对比 ID,不需要再使用 CAS 在进行操作。(偏向锁只进行一次 CAS 操作)
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象是偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。
如果不存在使用了,则可以将对象恢复成无锁状态,然后重新偏向。轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
作者:架构随笔
来源:CSDN
原文:https://blog.csdn.net/m631521383/article/details/87970244
版权声明:本文为博主原创文章,转载请附上博文链接!