synchronized 2022最新资料汇总
1.synchronized
1.1 什么是synchronized
一个JDK提供的同步的关键字,通过synchronized可以锁定一个代码块或者一个方法,从而实现锁的效果。
通过synchronized锁定的代码块或者方法同一时间只能由一个线程去执行,等这个线程执行完了释放锁了之后别的线程才能获取锁进入并且执行。
1.2 synchronized的几种用法
- 修饰普通方法
//this
public synchronized void testStaticSync() {
value++;
}
- 修饰静态方法
//class对象
public synchronized static void testStaticSync() {
value++;
}
- 修饰代码块
public class SynDemo {
int i = 0;
int x = 0;
Object lockObj = new Object();
public void testInnerSync() {
synchronized(lockObj) {
i++;
x++;
}
}
}
1.3 对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充
- 对象头:对象头由Mark Word 和 一个指向一个类对象的指针组成。
- 实例变量:存放这个实例的一些属性信息,比如有的属性是基本类型,那就直接存储值;如果是对象类型,存放的就是一个指向对象的内存地址。
- 对齐补充:主要是补齐作用,JVM对象的大小比如是8字节的整数倍,如果 (对象头 + 实例变量 )不是8的整数倍,则通过对齐填充来补齐。
1.3.1 Mark Word(重点)
这里Mark Word是一个32位的数据结构,存在于对象头里面。
- 由上图得知最后两位,也就是锁标志位,分别标识处于不同的锁模式;倒数第3位是偏向锁标志
2)当我的偏向锁标志是0,锁标志位是01,也就是最后3位是001的时候,我表示无锁模式。作为Mark Word的我就是记录的数据就是对象的hashcode 和 GC的年龄
3) 当我的偏向锁标志是1,锁标志是01,也就是最后三位是101的时候,处于偏向锁模式,我作为Mark Word这个时候记录的数据就是获取偏向锁的线程ID、Epoch(偏向时间戳)、对象GC年龄
4)当我的锁标志位是00的时候,表示处于轻量级锁模式。我会把锁记录放在加锁的线程的虚拟机栈空间中,所以这种情况下,锁记录在哪个线程虚拟机栈中,就表示所在线程就获取到了锁。
5)锁标志位是10的时候,表示处于重量级锁模式,这个时候就说明竞争激烈了,处于重量级锁模式了,由于使用重量级加锁不是java的职责范围,是底层c++的monitor的职责,前面则保存monitor的地址。
这个是我作为Mark Word 记录的数据就是monitor的地址,有加锁的需求直接根据记录的这个地址找到monitor,找它加锁就好了。
2.synchronized
2.1 monitor(重点)
2.1.1 monitor机制的概述
monitor是一个同步机制,或者一个同步的工具。synchronized底层就是使用了monitor来实现重量级锁的。
特点:
- 互斥:基于mutexlock, 只能有一个线程抢到锁
- signal机制:允许抢到锁的线程暂时放弃锁,等待某个条件触发后再去抢夺锁(wait、notify)
介绍:
- Monitor是依赖于底层操作系统实现,底层需要完成用户态到内核态转化,所以成本比较高,因此它是重量级锁
- Java与Monitor 每个java对象内置一个monitor对象,使用synchronized 锁对象可以是任意对象的原因就在于此,因此也叫内置锁。
实现:
-
monitor叫做对象监视器、也叫作监视器锁,JVM规定了每一个java对象都有一个monitor对象与之对应,这monitor是JVM帮我们创建的,在底层使用C++实现的。
ObjectMonitor() { _header; _count ; // 非常重要,表示锁计数器,_count = 0表示还没人加锁,_count > 0 表示加锁的次数 _waiters; _recursions; _owner; // 非常重要,指向加锁成功的线程,_owner = null 时候表示没人加锁 _waitset; // wait线程的集合,在synchorized代码块中调用wait()方法的线程会被加入到此集合中沉睡,等待别人叫醒它 _waitsetLock; _responsiable; _succ; _cxq; _freenext; _entrylist; // 非常重要,等待队列,加锁失败的线程会被加入到这个等待队列中,等待再次争抢锁 _spinFreq; // 获取锁之前的自旋的次数 _spinclock; // 获取之前每次锁自旋的时间 ownerIsThread; }
_count : 这个属性非常重要,直接表示有没有被加锁,如果没被线程加锁则 _count=0,如果_count大于0则说明被加锁了
**_owner:**这个属性也非常重要,直接指向加锁的线程,比如线程A获取锁成功了,则_owner = 线程A;当_owner = null的时候表示没线程加锁
**_waitset:**当持有锁的线程调用wait()方法的时候,那个线程就会释放锁,然后线程被加入到monitor的waitset集合中等待,然后线程就会被挂起。只有有别的线程调用notify将它唤醒。
**_entrylist:**这个就是等待队列,当线程加锁失败的时候被block住,然后线程会被加入到这个entrylist队列中,等待获取锁。
**_spinFreq:**获取锁失败前自旋的次数;JDK1.6之后对synchronized进行优化;原先JDK1.6以前,只要线程获取锁失败,线程立马被挂起,线程醒来的时候再去竞争锁,这样会导致频繁的上下文切换,性能太差了。
JDK1.6后优化了这个问题,就是线程获取锁失败之后,不会被立马挂起,而是每个一段时间都会重试去争抢一次,这个_spinFreq就是最大的重试次数,也就是自旋的次数,如果超过了这个次数抢不到,那线程只能沉睡了。
_spinClock:上面说获取锁失败每隔一段时间都会重试一次,这个属性就是自旋间隔的时间周期,比如50ms,那么就是每隔50ms就尝试一次获取锁。
1)首先呢,没有线程对monitor进行加锁
_count = 0 表示加锁次数是0,也就是没线程加锁;_owner 指向null,也就是没线程加锁
2)这个时候线程A、线程B来竞争加锁了,如下图所示:
在_count = 0,_owner = null的时候,表示monitor没人加锁,这个时候线程A和线程B同时请求加锁,也就是竞争将_count改为1,由于线程A这哥们动作比较快,它将_count改为1,获取锁成功了。它还嘚瑟了一下,同时将_onwer = 线程A,表示自己获取了锁,告诉线程B,兄弟不好意思了,是我获取了锁,我先去操作了。
3)如上图所示,线程A竞争到锁,将**_count 修改为1**,表示加锁次数为1,将_owner = 线程A,也就是指向自己,表示线程A获取到了锁。
- 释放锁:那反过来推测,释放锁的时候是不是将_count-- 直到 为0 , 将 _owner 设置为 null 就 OK了
总结:对象头、Mark Word 和 monitor之间的关系图
2.1.2 synchronized的Java代码体现
以下代码
public class Demo3 {
public void method2() {
synchronized (Demo3.class) {
System.out.println(123);
}
}
//ACC_synchronized
public synchronized void method1(String[] args) {
System.out.println(123);
}
}
JavaP命令反编译过后
method1(){
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: bipush 123
5: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
8: return
}
method2(){
0: ldc #2 // class com/itheima/demo1/Demo3
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: bipush 123
10: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
}
从上面反汇编结果可以看出:JVM对于同步方法和同步代码块的处理方式不同,对于同步方法,JVM采用ACC_SYNCHRONIZED
标记符来实现同步,而对于同步代码块,JVM则采用 monitorenter
和monitorexit
这两个指令实现同步。
monitorenter和monitorexit指令是什么?
可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 当一个线程获得锁(执行monitorenter)后,锁对象头MarkWord中记录的monitor的_count属性+ 1 ,当同一个线程再次获得该对象的锁的时候,该锁的monitor的_count计数器再次+1。当同一个线程释放锁(执行monitorexit指令)的时候,_count再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。
为什么monitorexit被执行两次?
但是细心的你是不是发现了上面出现了两条monitorexit
指令呢?这是为啥嘞?
是这样的,编译器需要确保方法中调用过的每条monitorenter指令都要执行对应的monitorexit 指令。为了保证在方法异常时,monitorenter和monitorexit指令也能正常配对执行,编译器会自动产生一个异常处理器,它的目的就是用来执行异常的monitorexit指令。而字节码中多出的monitorexit指令,就是异常结束时,被执行用来释放monitor的。
异常会释放锁吗?
上述问题已经给出了答案。会的,第二个monitorexit就是用于异常情况下释放锁的。
monitor的其他属性
_spinFreq: 等待锁期间自旋的次数
_spinclock: 自旋的周期
_entrylist: 自旋次数用完了还没获取锁,只能放到**_entrylist等待队列挂起了**
如下图:
(1)首先线程B获取锁的时候发现monitor已经被线程A加锁了
(2)然后monitor里面记录的_spinFreq 、spinclock 信息告诉线程B,你可以每隔50ms来尝试加锁一次,总共可以尝试10次
(3)如果线程B在10次尝试加锁期间,获取锁成功了,那线程B将_count 设置为 1,_owner 指向自己表示自己获取锁成功了
(4)如果10次尝试获取锁此时都用完了,那没辙了,它只能放到等待队列里面先睡觉去了,也就是线程B被挂起了
2.2 synchronized保证原子性
synchronized通过monitor监视器来保证只能有一个线程抢到锁,抢到锁的线程才能执行代码,从而确保原子性。
2.3 synchronized可见性
在释放锁之前一定会将数据写回主内存
:
一旦一个代码块或者方法被Synchronized所修饰,那么它执行完毕之后,被锁住的对象所做的任何修改都要在释放之前,从线程内存写回到主内存。也就是说他不会存在线程内存和主内存内容不一致的情况。
在释放锁之前一定会将数据写回主内存
:
同样的,线程在进入代码块得到锁之后,被锁定的对象的数据也是直接从主内存中读取出来的,由于上一个线程在释放的时候会把修改好的内容回写到主内存,所以线程从主内存中读取到数据一定是最新的。
2.4 synchronized有序性
synchronized满足有序性,但是
Java 里只有 volatile 变量是能实现禁止指令重排的,synchronized 虽然不能禁止指令重排,但也能保证有序性。
synchronized 和 volatile 的有序性与可见性是两个角度来看的:
-
synchronized 是因为synchronized 块与synchronized 块之间看起来是原子操作,块与块之间有序可见
-
volatile 是在底层通过内存屏障防止指令重排的,变量前后之间的指令与指令之间有序可见
同时,synchronized 和 volatile 有序性不同也是因为其实现原理不同:
- synchronized 靠操作系统内核互斥锁实现的。退出代码块时一定会刷新变量回主内存
- volatile 靠插入内存屏障指令防止其后面的指令跑到它前面去了,粒度更细
总而言之就是, synchronized 块里的非原子操作依旧可能发生指令重排
在外部使用了synchronized范围内的变量也有可能出现有序性问题。这个时候就要配合volatile来使用了,典型例子就是 double check实现的单例模式,网址链接:https://blog.csdn.net/weixin_38898423/article/details/106639998
3.锁升级
第一个线程获取偏向锁,CAS记录当前线程id,锁标识101
第二个线程过来,偏向锁记录不是自己的线程id,锁竞争升级为轻量级锁,锁标识为00
第三个线程过来,获取轻量级锁,没抢到自旋,自旋结束没抢到,升级为重量级锁,标识为10
偏向锁、轻量级锁都是jvm控制的,重量级锁是底层操作系统monitor基于mutexlock支撑的,涉及到用户态和内核态的切换,性能低。