目录
https://www.jb51.net/article/183984.htm
https://blog.csdn.net/mulinsen77/article/details/88635558
https://www.yuque.com/wanghuaihoho/gobbon/szufr0
https://blog.csdn.net/tongdanping/article/details/79647337
https://www.bilibili.com/video/BV1tz411q7c2?p=1
1、用户态与内核态
现在的操作系统(linux,windows),一般分为内核态kernel
和用户态(普通的用户程序),所有用户态的程序要访问硬盘,内存,网卡等,必须经过内核态的允许,内核态可以执行所有指令,用户程序要执行指令必须经过内核态来调用,JVM
对于操作系统来说也就是一个普通程序,因此,如果jvm
要操作内存必须经过内核态的允许。
早期的synchronized
是重量级锁,对于某个资源加锁的时候,需要经过操作系统的老大 - 内核态的允许(锁属于操作系统的核心资源,准确的说是互斥锁mutex
),经过内核态的线程调度,才能拿到锁,这把锁叫重量级锁,重是因为要经过内核态允许的这一个步骤。
后面对synchronized
进行了优化,在某些特定条件下,不需要经过内核态的允许,只需要在用户空间就能解决,比如:cas
只是从内存中读取值,然后进行比较,因此,没必要经过内核态的允许这一步骤,因此,cas
叫轻量级锁。
因此,早期synchronized
效率低的是因为要经过内核态。
2、使用工具查看对象的内存布局
主要用来观察对象内部怎么实现。
依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
实例1:
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
结果:
实例2:如何读取VALUE
public class HelloJol {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
//锁定某个对象,实际上是修改对象的MarkWork内容
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
header
中前8个字节按照平时习惯的从高位到低位的展示为:
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
后两位为01,前面又是0,对照表(在下面),是无锁
00000000 00000000 00000000 00000000 00000011 00001001 11110100 10101000
后两位为00,是轻量级锁
普通对象加synchronized
,直接升级为轻量级锁。
3、synchronized字节码
- 当执行
monitorenter
指令时,当前线程将试图获取对象锁所对应的monitor
的持有权,当该对象的monitor
的计数器为0,那线程可以成功取得monitor
,并将计数器值设置为1,取锁成功。 - 如果当前线程已经拥有
monitor
的持有权,那它可以重入这个monitor
,重入时计数器的值也会加 1。 - 倘若其他线程已经拥有该对象的
monitor
的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit
指令被执行,执行线程将释放monitor
(锁)并设置计数器值为 0 ,其他线程将有机会持有monitor
。
4、监视器对象(管程对象或Monitor)
JVM
中的同步是基于进入与退出监视器对象(管程对象)(Monitor
)来实现的,每个对象实例都会有一个Monitor
对象,Monitor
对象会和Java
对象一同创建并销毁。Monitor
对象是由C++
来实现的。
这就是为什么任何对象都可以作为锁,线程在获取锁的时候,实际上就是获得一个监视器对象 (monitor
) ,monitor
可以认为是一个同步对象,所有的Java
对象是天生携带monitor
Monitor
源码:
ObjectMonitor() {
_header = NULL;
_count = 0; //重入时的计数器
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //_owner指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
1、EntryList阻塞队列
当多个线程同时访问一段同步代码时,这些线程会被放到一个EntryList
集合中,处于阻塞状态的线程都会被放到该列表当中。接下来,当线程获取到对象的Monitor
时,Monitor
是依赖于底层操作系统的mutex lock
(互斥锁)来实现互斥的,线程获取mutex
成功,则会持有该mutex
,这时其他线程就无法再获取到该mutex
。
2、WaitSet等待集合
如果线程调用了wait
方法,那么该线程就会释放掉所持有的mutex
,并且该线程会进入到WaitSet
集合(等待集合)中,等待下一次被其他线程调用notify/notifyAll
唤醒。如果当前线程顺利执行完毕方法,那么它也会释放掉所持有的mutex
。
5、锁升级
1、JDK对锁的优化
从JDK 1.5
开始,并发包引入了Lock
锁。
Lock
同步锁是基于Java
来实现的,因此Lock
锁的获取与释放都是通过Java
代码来实现与控制的;synchronized
是基于底层操作系统的Mutex Lock
来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这种切换会极大地增加系统的负担;在并发量较高时,也就是说锁的竞争比较激烈时,synchronized
锁在性能上的表现就非常差。
从JDK 1.6
开始,synchronized
锁的实现发生了很大的变化;JVM
引入了相应的优化手段来提升synchronized
锁的性能,这种提升涉及到偏向锁、轻量级锁及重量级锁等,从而减少锁的竞争所带来的用户态与内核态之间的切换;这种锁的优化实际上是通过Java
对象头中的一些标志位来去实现的;对于锁的访问与改变,实际上都与Java
对象头息息相关。
2、锁对象头 - Mark Work部分
对象实例在堆当中会被划分为三个组成部分:对象头、实例数据与对齐填充。
对象头主要也是由3块内容来构成:Mark Word
(标记词)、指向类的指针、数组长度
Mark Word
包含:锁信息,GC
分代年龄,hashCode
Mark Word
:8字节,64位,一个字节八位
- 无锁:没有锁对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。其他修改失败的线程会不断重试,直到修改成功,如
CAS
原理和应用是无锁的实现。 - 偏向锁:偏向锁是指一段同步代码一直被一个线程访问,那个该线程会自动获取锁,降低获取锁的代价。当前线程把自己的
id
贴到了markwork
上。 - 轻量级锁(也叫自旋锁):是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。通过cas操作和自旋来解决加锁问题,自旋超过一定的次数或者已经有一个线程在自旋,又来一个线程获取锁时,轻量级锁会升级为重量级锁。
- 重量级锁:升级为重量级锁,等待锁的线程都会进入阻塞状态。锁标识位为10,其中指针指向的是
monitor
对象(也称为管程或监视器锁)的起始地址。
偏向锁、自旋锁都是用户空间完成
重量级锁需要向内核态申请
3、synchornized锁的升级
对于synchronized
锁来说,锁的升级主要都是通过Mark Word
中的锁标志位与是否是偏向锁标志位来达成的;
synchronized
关键字所对应的锁都是先从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后则变成了重量级锁。
锁的升级是单向的,不会出现锁的降级。
对于锁的演化来说,它会经历如下阶段:无锁 -> 偏向锁 -> 轻量级锁(自旋锁) -> 重量级锁
1、无锁状态
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
hash
值为中间31
位:01111011001000111110110010000001
2、偏向锁
偏向锁主要作用就是优化同一个线程多次获取一个锁的情况;
偏向锁由两个字段来控制:
• 偏向锁标记字段
• Thread ID(线程ID)字段
偏向锁的加锁:
- 一个
synchronized
方法被一个线程访问,那么这个方法所在的对象就会在其Mark Word
中将偏向锁进行标记,同时还会有一个字段来存储该线程的ID
; - 当这个线程再次访问同一个
synchronized
方法时,它会检查这个对象的Mark Word
的偏向锁标记以及是否指向了其线程ID
,如果是的话,那么该线程就无需再去进入管程(Monitor
)了,而是直接进入到该方法体中。
偏向锁的jvm
参数设置?
jvm
是默认开启偏向锁,但有5s
的延迟。
开启偏向锁:-XX:+UseBiasedLocking
设置偏向锁延迟为0,即关闭延迟:-XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
演示偏量级锁:
//-XX:BiasedLockingStartupDelay=0
public class HelloJol {
public static void main(String[] args) throws InterruptedException {
System.out.println("当前线程id:"+Thread.currentThread().getId());
//匿名偏向锁
//0000000000000000000000000000000000000000000000
//0
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
//偏向锁
//000000000000000000000000000000000000001011010001010000
//46160
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
第一次打印为匿名偏向,第二次偏向锁指向了main
线程
偏向锁线程id
:000000000000000000000000000000000000001011010001010000
转成十进制46160
,打印的结果是显示是线程id=1
,怎么回事呢?
代码Thread.currentThread().getId()
获取的threadId
其实是jvm
里的线程id
,和我们常说的linux
系统线程id
不一样
更多演示:https://www.jb51.net/article/183984.htm
3、轻量级锁
偏量级锁升级为轻量级锁只需要两个线程,第一个线程已经获取到了当前对象的锁并且是偏向锁,第二个线程在抢时,发现Mark Word
里面存储的线程ID
并不是自己(是第一个线程),那么它会进行CAS(Compare and Swap)
,从而获取到锁,这里面存在两种情况:
- 获取锁成功:那么它会直接将
Mark Word
中的线程ID
由第一个线程变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态。 - 获取锁失败:(自旋次数达到界限值)则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时偏向锁就会进行锁撤销,升级为轻量级锁。
- 如果是线程1获取到轻量级锁会先把锁对象的对象头
MarkWord
复制一份到线程1的栈帧中创建的用于存储锁记录的空间(官方称为DisplacedMarkWord
),然后使用CAS
把对象头中的指针指向线程1存储的锁记录空间的地址;别的线程试图抢锁的时候,发现MarkWord
上的指针已经被换了,CAS
失败,那么线程就尝试使用自旋锁来等待线程1释放锁。 JVM
对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。
4、重量级锁
JDK1.5
之前默认的锁的形态。
如果多个线程竞争锁,轻量级锁要膨胀为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor
重量级锁通过对象内部的监视器(Monitor
)实现,其中 Monitor
的本质是依赖于底层操作系统的Mutex Lock
实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
6、锁消除技术与逃逸分析
JIT
编译器在动态编译同步块的时候,可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
代码示例:
/**
* 同步省略说明
*/
public class SynchronizedTest {
public void f() {
Object hollis = new Object();
synchronized(hollis) {
System.out.println(hollis);
}
}
//代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中
//并不会被其他线程所访问控制,所以在JIT编译阶段就会被优化掉。
//优化为 ↓
public void f2() {
Object hollis = new Object();
System.out.println(hollis);
}
}
https://blog.csdn.net/weixin_42412601/article/details/107533660
7、锁粗化
JIT
编译器在执行动态编译时,若发现前后相邻的synchronized
块使用的是同一个锁对象,那么它就会把这几个synchronized
块给合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就无需频繁申请与释放锁了,从而达到申请与释放锁一次,就可以执行完全部的同步代码块,从而提升了性能。
public class MyTest5 {
private Object object = new Object();
public void method() {
synchronized (object) {
System.out.println("hello world");
}
synchronized (object) {
System.out.println("welcome");
}
synchronized (object) {
System.out.println("person");
}
}
}
8、synchronized的执行过程
检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
如果自旋成功则依然处于轻量级状态。
如果自旋失败,则升级为重量级锁。