前言
在java 开发中对于锁的应用非常的常见,如果对于什么时候该用什么锁,以及锁实现的原理有所不知道的,或者面试过程中面试官问你不知道怎么回答的,欢迎来看下面的文章
1、synchronized和ReentrantLock的区别
2、synchronized的一些特性和底层原理的实现
2.1 synchronized 的锁升级过程
在 JVM 中,锁升级是不可逆的,即一旦锁被升级为下一个级别的锁,就无法再降级。
首先默认的无锁状态,当我们加锁以后,可能并没有多个线程去竞争锁,此时我们可以默认为只有一个线程要获取锁,即偏向锁,当锁转为偏向锁以后,被偏向的线程在获取锁的时候就不需要竞争,可以直接执行。
当确实存在少量线程竞争锁的情况时,偏向锁显然不能再继续使用了,但是如果直接调用重量级锁在轻量锁竞争的情况下并不划算,因为竞争压力不大,所以往往需要频繁的阻塞和唤醒线程,这个过程需要调用操作系统的函数去切换 CPU 状态从用户态转为核心态。因此,可以直接令等待的线程自旋,避免频繁的阻塞唤醒 ,此时升级为轻量级锁。
当竞争加大时,线程往往要等待比较长的时间才能获得锁,此时在等待期间保持自旋会白白占用 CPU 时间,此时就需要升级为重量级锁,即 Monitor 锁,JVM 通过指令调用操作系统函数阻塞和唤醒线程。
2.2 synchronized的锁优化
2.2.1 自适应自旋锁
自旋锁依赖于 CAS,我们可以手动的设置 JVM 的自旋锁自旋次数,但是往往很难确定适当的自旋次数,如果自旋次数太少,那么可能会引起不必要的锁升级,而自旋次数太长,又会影响性能。在 JDK6 中,引入了自适应自旋锁的机制,对于同一把锁,当线程通过自旋获取锁成功了,那么下一次自旋次数就会增加,而相反,如果自旋锁获取失败了,那么下一次在获取锁的时候就会减少自旋次数。
2.2.2 锁消除
在一些方法中,有些加锁的代码实际上是永远不会出现锁竞争的,比如 Vector 和 Hashtable 等类的方法都使用 synchronized 修饰,但是实际上在单线程程序中调用方法,JVM 会检查是否存在可能的锁竞争,如果不存在,会自动消除代码中的加锁操作。
2.2.3 锁粗化
我们常说,锁的粒度往往越细越好,但是一些不恰当的范围可能反而引起更频繁的加锁解锁操作,比如在迭代中加锁,JVM 会检测同一个对象是否在同一段代码中被频繁加锁解锁,从而主动扩大锁范围,避免这种情况的发生。
2.3 synchronized的底层原理的实现
synchronized 意为同步,它可以用于修饰静态方法,实例方法,或者一段代码块。其中修饰代码块和方法有一点不同
它是一种可重入的对象锁。当修饰静态方法时,锁对象为类;当修饰实例方法时,锁对象为实例;当修饰代码块时,锁可以是任何非 null 的对象。由于其底层的实现机制,synchronized 的锁又称为监视器锁。
同步代码块
public void synchronizedMethod3() {synchronized(this) {System.out.println("synchronizedMethod3");}}
反编译之后:
monitorenter // 尝试获取对象锁
...
monitorexit // 正常退出同步块(偏移量 15)
goto 23
monitorexit // 异常退出同步块(偏移量 21)
athrow
这里的 monitorenter 与 monitorexit 即是线程获取 synchronized 锁的过程。
当线程试图获取对象锁的时候,根据 monitorenter 指令:
如果 Monitor 的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为 Monitor 的所有者;
如果线程已经占有该 monitor,只是重新进入,则进入 Monitor 的进入数加1(可重入); 如果其他线程已经占用了
monitor,则该线程进入阻塞状态,直到 Monitor 的进入数为0,再重新尝试获取 Monitor 的所有权; 当线程执行完以后,根据
monitorexit 指令:当执行 monitorexit 指令后,Monitor 的进入数 -1; 如果 – 1 后 Monitor 进入数为 0,则该线程不再拥有这个锁,退出 monitor; 如果 – 1 后 Monitor 进入数仍不为0,则线程继续持有这个锁,重复上述过程直到使用完毕。
特点:
- 显式生成 monitorenter 和 monitorexit 指令 进行获取锁和释放锁
- 异常表中注册两个退出路径,确保锁必然释放
同步方法
public synchronized void syncMethod() { System.out.println("同步方法");
}
关键字节码:flags: ACC_PUBLIC, ACC_SYNCHRONIZED
通过 ACC_SYNCHRONIZED 标志隐式实现锁机制是 Java 中 synchronized 关键字的核心实现原理。 在 Java
虚拟机(JVM)中,synchronized 关键字修饰的方法或代码块会通过在方法的 access_flags 字段中添加
ACC_SYNCHRONIZED 标志来标识该方法为同步方法。这个标志告诉 JVM,该方法需要进行同步操作。同步机制的隐式实现 当一个方法被标记为 ACC_SYNCHRONIZED 时,JVM 在方法调用时会隐式地执行加锁和解锁操作。具体来说:
加锁:在方法执行前,线程需要获取与该方法相关的监视器锁(monitor)。如果锁已被其他线程持有,则当前线程会被阻塞,直到锁被释放。
解锁:当方法正常完成或抛出异常时,锁会被自动释放。这确保了方法的原子性和可见性。
特点:
- 通过 ACC_SYNCHRONIZED 标志隐式实现锁机制
- JVM 在方法调用和返回时自动插入锁操作
实现原理
synchronized 是对象锁,在 JDK6 引入锁升级机制后,synchronized 的锁实际上分为了偏向锁、轻量级锁和重量级锁三种,这三者都依赖于对象头中 MarkWord 的数据的改变。
对象
每个 Java 对象在内存中分为 对象头(Header) 、 实例数据(Instance Data) 和 对齐填充(Padding)
对象头是实现锁的核心区域,其结构如下
其中markWord的样子如下:
2.3 重量级锁与监视器
synchronized 的对象锁是基于监视器对象 Monitor 实现的,而根据上文,我们知道锁信息存储于对象自己的 MarkWord 中,那么 Monitor 和 对象又是什么关系呢?
实际上,在对象在创建之初就会在 MarkWord 中关联一个 Monitor 对象 ,当锁升级到重量级锁时,markWord存储指向 Monitor 对象的指针。
Monitor 对象在 JVM 中基于 ObjectMonitor 实现,代码如下:
ObjectMonitor() {_header = NULL;_count = 0; // 持有锁次数_waiters = 0,_recursions = 0;_object = NULL;_owner = NULL; // 当前持有锁的线程_WaitSet = NULL; // 等待队列,处于wait状态的线程,会被加入到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ; // 阻塞队列,处于等待锁block状态的线程,会被加入到该列表_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;
}
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter 对象 ),_owner 指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码时:
首先会进入 _EntryList 集合,当线程获取到对象的 Monitor 后,进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程,同时 monitor中的计数器 count 加1;
若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减1,同时该线程进入 WaitSet集合中等待被唤醒;
若当前线程执行完毕,也将释放 Monitor 并复位 count 的值,以便其他线程进入获取 Monitor;
这也解释了为什么 notify() 、notifyAll()和wait() 方法会要求在同步块中使用,因为这三个方法都需要获取 Monitor 对象,而要获取 Monitor,就必须使用 monitorenter指令。
2.4 synchronized线程调用时的阻塞场景
public class Example {public synchronized void methodA() { ... }public synchronized void methodB() { ... }
}
/线程1调用obj.methodA(),线程2调用obj.methodB() → 线程2阻塞,直到methodA释放锁
所以再用的时候尽可能的缩小锁的范围,代码块加锁
总结
每个对象在内存中分为三个部分示例数据(存放类信息)、对象头和对齐填充,其中对象头有个重要的组成部分markWord,markWord中存储一些锁相关的引用,通过markWord中的锁标志位,我们可以清楚的知道锁的级别,是重量级锁,还是轻量级锁,还是偏向锁,当是重量级锁的时候,markWord中存储了对监视器Monitor 对象的引用,synchronized 的对象锁是基于监视器对象 Monitor 实现的,在对象在创建之初就会在 MarkWord 中关联一个 Monitor 对象 ,当锁升级到重量级锁时,markWord存储指向 Monitor 对象的指针。而要获取 Monitor,就必须使用 monitorenter指令,可重入的表现是,Monitor里面有一个count,一次monitorenter后count 加1,一次monitorExit 后count减1
参考文章:https://cloud.tencent.com/developer/article/2120268?pos=comment