Java synchronized底层原理与底层锁优化
synchronized发生异常会自动释放锁
在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的互斥锁来实现的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
使用方式
- 修饰实例方法方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。synchronized(this)。
- **修饰静态方法: ** 也就是给当前类加锁,会作用于类的所有对象实例。
因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 - **修饰代码块: ** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。最好别用字符串,因为JVM中,字符串常量池具有缓存功能。
双重校验锁实现对象单例(线程安全):
通过volatile保证线程的可见性并防止指令的重新排序,synchronized加锁防止重复实例化。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
synchronized 关键字的底层原理
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
public synchronized void synchronizedMethod() {
System.out.println("synchronized 方法");
}
}
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java
命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。
synchronized代码块:
synchronized方法:
synchronized代码块
**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 **
当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
monitor原理及执行步骤
因为每个对象头中都存在monitor锁对象,所以java中可以用对象加锁。
对象结构大概分为对象头、实例变量和填充字节。
- 对象头
- Mark Word(标记字段):存储对象的hashCode、锁信息或分代年龄或GC标志等信息
- Klass Pointer(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 数组长度:只有数组对象保存了这部分数据。该数据在32位和64位JVM中长度都是32bit。
- 实例变量:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
- 填充字节:第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,查看部分ObjectMonitor.cpp源码:
ObjectMonitor () {
// 指向持有ObjectMonitor对象的线程地址。
_owner = NULL;
// 存放调用wait方法,而进入等待状态的线程的队列。
_WaitSet = NULL
// 这里是等待锁block状态的线程的队列。
_EntryList = NULL;
// 锁的重入次数。
_recursions = 0;
// 线程获取锁的次数。
_count = 0;
}
执行步骤:
- 当多个线程同时访问时,这些线程先被放进**_EntryList队列**中,线程处于blocked状态。
- 当其中的一个线程获取到对象的monitor后,该线程进入running状态,_owner指向当前线程,_count加1表示锁被一个线程获取到。
- 当running状态线程调用wait()方法,释放当前线程的monitor对,并进入waiting状态。_owner变为NULL,_count减1,将线程加入都_WaitSet队列中,当有线程notify()该线程时,将该线程加入到_EntryList队列中参与锁的竞争。
- 当线程执行完成时,释放monitor对象,_owner为NULL,_count减1。
synchronized方法
方法头部使用的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
JDK1.6之后synchronized优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
如果不了解,请点击学习:Java中的锁膨胀、锁消除、锁粗化、自旋锁、偏向锁、轻量级锁、重量级锁详解
本文地址:https://blog.csdn.net/weixin_41590779/article/details/107618780
推荐阅读
-
荐 JVM虚拟机底层原理分析与性能优化思想
-
Java并发编程深入理解之Synchronized的使用及底层原理详解 下
-
Java synchronized底层原理与底层锁优化
-
Java并发编程深入理解之Synchronized的使用及底层原理详解 上
-
Java synchronized底层实现原理以及锁优化
-
Java集合——HashMap底层实现与原理源码分析——JDK1.8
-
荐 JVM虚拟机底层原理分析与性能优化思想
-
Java synchronized底层原理与底层锁优化
-
Java并发编程深入理解之Synchronized的使用及底层原理详解 下
-
Java并发编程深入理解之Synchronized的使用及底层原理详解 上