【一些面试真题】:
阿里P9——0x80的执行过程。
【 重温CAS过程 】:
【硬件】:
Lock指令在执行后面指令的时候锁定一个北桥信号(不采用锁总线的方式)。
【用户态 与 内核态】:
作为操作系统来说,它能够做的操作是不允许普通程序去做的。为了保障操作系统的健壮性,现代的操作系统都会把指令分成级别。
内核态是执行在内核空间的,它能够访问所有的指令。
而用户态的程序只能访问用户能够访问的指令。
//对于操作系统来说 , 你JVM也仅仅是工作在用户态的。
【 重量级锁 】:
锁这个资源是需要通过操作系统才能够去申请到的。称之为重量级锁的原因是——申请、返回 都需要经过操作系统。
【 早期上重量级锁 】:
JVM要对某个资源加锁了 ,你对资源加锁的时候 , 你得向操作系统申请一把锁——完成从用户态到内核态的调用。
【中断的调用】:
从JVM到OS申请的时候,是要经过一个中断的调用。返回锁的时候,也是要从内核态返回到用户态。
【做的优化】:
某些状态下上锁的话 , 是无需向操作系统申请的,只需要在用户态就可以解决问题。
【JOL对象内存布局】:
JOL = Java Object Layout
一个对象在内存中如何分布这件事情,是和特定的JVM虚拟机实现有关系的,我们今天讲的主要是Hotspot的实现 ,其它的虚拟机有可能不是这种实现。
【四部分构成】:
markword 8、 classpointer 4 、 instance data(看实际的变量类型)、 padding(补齐)。
【markword中】:
其中,最重要的信息就是——锁信息。
【详解锁升级过程】:
【锁升级实质】:
通过markword的后几位标志来实现升级的;
【锁升级图】:
锁升级初步
//当我们new出一个普通对象的时候,它有可能是两种状态,两种状态的意思是——普通对象/匿名偏向 两种类型的markword是不同的形式,new Object就是一个普通对象。
【new Object】:
我们new Object , 一旦我给这个对象加上了synchronized关键字的时候 , 它会首先升级成偏向锁 ,竞争一旦激烈就会升级成轻量级锁,轻量级锁又叫做自旋锁,竞争再加剧会变成重量级锁——也就是一开始我们提到的,向操作系统老大申请的锁。
【偏向锁未启动】:
偏向锁没有启动 , 普通对象也是有可能直接升级成轻量级锁的。
【 如何区分锁的状态 】:
00——轻量级锁;
10——重量级锁;
11——GC标记信息;
01——必须得靠偏向锁位来区分。
001——无锁状态。
101——偏向锁状态。
【锁的介绍】:
【用户空间锁】:
偏向锁 和 轻量级锁 都是用户空间锁(用户态度锁)—— 我不需要和操作系统打交道。
【重量级锁VS用户空间锁】:
//偏向锁——哪个线程先来,我就偏向它就可以了。
【StringBuffer】:
里面的方法是全都加synchronized的。
【形象理解锁机制】:
【偏向锁】:
markWord中放的是当前线程指针,所谓的偏向锁是没有必要设计锁竞争机制的,第一个访问这把锁的线程直接把自己的线程ID往上一贴就完了。总而言之是贴上当前线程的标志。
【自旋锁】:
先撤销偏向锁之前贴上的线程标识 ,撤销掉之后进行竞争 ,竞争的方式就是——自旋的竞争 ;
LockRecord
每个线程在它的线程栈中生成一个LR(锁记录),将这个LR贴到锁上,锁上的指针指向哪一个线程的LR , 就表示哪个线程持有这把锁。另外的线程只能用CAS机制继续竞争。
【重量级锁】:
//我这把锁必须得向操作系统去申请;MarkWord中实际记录的是一个ObjectMoint——实际就是JVM空间写的一个C++对象 , 而这个C++对象它内部去访问的时候是需要通过操作系统,经过操作系统之后拿到操作系统对应的那一把锁。
【Hotspot源码位置】:
InterpreterRuntime::monitorenter方法。
【synchronized编译过程】:
当我们在Java文件中写了synchronized{…} 代码块之后,在汇编语句中就会出现:
monitorenter——原 { 位置处,锁开始。
monitorexit—–—原 } 位置出,锁结束。
最后一个monitorexit—–—产生任何异常的话。
//最后一个monitorexit是表示——产生了异常,如果发生异常自动释放锁。
【monitorenter】:
//如果使用了偏向锁 ( 618行的判断 ), 也就是如果偏向锁打开了 ,fast_enter——快速地竞争锁;否则slow_enter。
//首先进入自旋;——升级为自旋锁。
//如果自旋锁不成——锁膨胀。inflate方法——膨胀为重量级锁。
【重量级锁的代码】
synchronizer.cpp 中。
【 锁重入 】:
sychronized是可重入锁。
重入次数必须记录,因为要解锁几次必须得对应。
重入次数记录在哪里呢???
——不同锁的实现是不一样的,偏向锁记录在线程栈里,每增加一次,LR+1。
偏向锁、自旋锁 -> 线程栈 -> LR+1 。
重量级锁 -> ObjectMonitor 字段上。
【概念简介】:
方法m( ) 是 synchronized 修饰的 ,方法m加的锁是O , 方法m里面调用了方法 n( ) , 方法n( ) 加的锁也是O , 所以就相当于给锁O上了两次。
【自旋锁什么时候升级成重量级锁?】:
竞争加剧,有线程超过10次自旋,或者自旋线程数超过CPU核数的一半。
【 偏向锁启动 】:
【为什么有自旋锁还需要重量级锁?】:
自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。
//时间都花在线程的自旋上了。
重量级锁里面有各种各样的队列。每个队列的作用都是不同的(竞争 / 执行 / 等待)。
一个线程想申请一把重量级锁,会进入一个队列里。假如10000个线程在等待,这10000个线程会在WaitSet里 ,不需要消耗CPU时间,所以在竞争超级激烈的时候重量级锁会比自旋锁更合适。Linux内核对于进程的调度叫做CFS 。
重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源。
【偏向锁是否一定比自旋锁效率高?】:
不一定。
在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接关闭偏向锁,这时候直接使用自旋锁。
JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开。
//偏向锁启动的时候,默认有一个时延。
【匿名偏向】:
刚开始偏向锁还没有偏向任何一个线程,所以称之为匿名偏向。
【 偏向锁直接升级成重量级锁 】:
调用了wait( ) 方法,直接进入重量级锁状态。
【所有锁升级路线】:
- new -> 普通对象
- 普通对象 -> 轻量级锁
- new -> 匿名偏向
- 匿名偏向 -> 偏向锁
- 偏向锁 -> 轻量级锁
- 偏向锁 -> 重量级锁
- 轻量级锁 -> 重量级锁
【普通对象到偏向锁】:
关键在于——Epoch。( 批量重偏向与批量撤销 )
【例】:
一个Class , 有很多的对象( 1~19 )。
//第一个线程来的时候,会把锁全都给加上。( 20个同一个类的对象 )
第二个线程来竞争这个锁的时候——它会直接升级成为《自旋锁》。从第21个对象开始,如果线程又给这个对象来上锁,上的会是偏向锁。——这个锁叫做重偏向。因为它原先偏向过第一个线程,批量重偏向。本来这个锁已经升级到了《轻量级锁》 ,JVM观测到你这个Class对应的对象竞争非常的多,这个时候给你又回到偏向锁这种效率比较高的机制上来 ,这个优化是针对于Class来进行的,它并不是针对于对象进行的,前面讲的都是针对于对象来讲的,而这个优化是针对于Class来说的,线程继续加锁 , 当加到第41个的时候,我认为加偏向锁是有问题的,这个时候就会进行批量锁撤销,会把你以前锁定的这些全部撤销,全升级为《自旋锁》。———批量锁的重偏向、批量锁的撤销。
[ 复习批量重偏向 、批量锁撤销 概念 ]:
Class生产的所有对象 ,把你对象的状态全部都换成偏向锁。
如果说你的对象特别多 ,说明竞争非常激烈 ,到第40个的时候,全部给你换成轻量级锁。
//这是一个针对Class的操作。