目录
以下知识基于HotSpot虚拟机实现
1.前置知识
1.1 锁的作用
1.2 Java中常见的锁类型
1.3 锁的重入
2.使用场景
2.1 修饰实例方法
2.1.1 用法
2.1.2 原理
2.1.3 特点
2.2 修饰静态方法
2.2.1 用法
2.2.2 原理
2.3 修饰代码块
2.3.1 用法
3.原理
3.1 对象锁
3.1.1 对象锁的实现原理
3.1.1.1 无锁
3.1.1.2 偏向锁
3.1.1.3 轻量级锁
3.1.1.4 重量级锁
3.1.1.2.1 Monitor
3.1.2 锁升级
3.1.3 锁消除
3.1.4 锁粗化
3.2 对象头
3.2.1 对象头的结构
3.2.1.1 MarkWord
PS:这里分代年龄是4位bit,也说明了为什么分代年龄最大是15。
3.2.1.1 指向类的指针(class pointer)
以下知识基于HotSpot虚拟机实现
1.前置知识
1.1 锁的作用
锁是一种同步机制,可以用来协调多个线程的并发访问,以保证对共享资源的安全访问。可以理解为防止一件东西同时被多个人使用。
1.2 Java中常见的锁类型
Java中常见的锁类型-CSDN博客
1.3 锁的重入
锁的重入是指在一个线程持有锁的情况下,可以重复获取同一个锁,而不会发生死锁。正常来说a获取到锁后b不能获取锁,但是当一个实例内的a方法和b方法都是用一把锁上锁时,同一个线程访问a和b就需要重入的能力,比如在a方法中访问b方法,那么a的锁x已经被抢占了,如果不支持重入,那么访问b的时候会发现x锁已经被自己抢占而无法访问,支持重入的原理也就是在这里判断一下当前方法的锁是否是本线程拥有的锁。
2.使用场景
2.1 修饰实例方法
2.1.1 用法
public class SynchronizedExample {// 使用synchronized修饰的实例方法public synchronized void increment() {}
}
2.1.2 原理
Java通过锁住此方法对应的对象锁实现同步访问。具体查看3.1
2.1.3 特点
由于锁定的是对象锁,那么会有一下特点
1.同一个实例对象下被Synchronized修饰的方法会互相影响,也就是说当访问同一个实例对象中被Synchronized修饰的a方法时,其他线程无法访问这个实例对象下被被Synchronized修饰的b方法。
2.被Synchronized修饰的方法与未被Synchronized修饰的方法互不影响
3.但是如果是同一线程访问a方法后和b方法,不会被阻塞,因为Synchronized支持重入(查看1.3)
2.2 修饰静态方法
2.2.1 用法
synchronized void staic method() {
}
2.2.2 原理
Java通过锁住此方法对应的类的对象锁实现同步访问。具体查看3.1
静态同步方法, 锁是当前类的Class对象。一个类只有一个类对象,类对象的锁也是和3.1中对象锁原理一样
2.3 修饰代码块
2.3.1 用法
synchronized(this) {
}
锁是synchronized括号里实例的对象。
synchronized(a.class) {
}
锁是synchronized括号里类的class对象。
3.原理
这里需要一些前置知识,每一个对象可以关联一个ObjectMonitor对象,对象中有个对象头的区域,Java在不同场景下通过利用对象头以及ObjectMonitor对象来实现对象锁。
3.1 对象锁
JVM在JDK1.6后会有几种状态,线程获取对象时,分别会判断对象锁的状态,以此来决定线程是否进入阻塞,或者获取到锁对象执行代码块,或者自旋等接下来的动作。
对象锁是Java中用于实现同步的机制之一,它可以确保在多线程环境下对共享资源的访问是安全的。对象锁的重量级锁状态是基于对象的监视器(monitor)实现的,在Java中,每个对象都有一个与之关联的监视器,用于实现同步。
3.1.1 对象锁的实现原理
最开始JDK1.5以及以前,java只有重量级锁。
从JDK1.6开始,对象锁有4种状态,无锁,偏向锁,轻量级锁,重量级锁。
这四种状态由3.2.1.1 中的MarkWord内的是否偏向锁或锁标志位确定。
先了解四种状态的特点
3.1.1.1 无锁
当MarkWord的是否偏向锁位为0,锁标志位为01时。对象锁为无锁状态
3.1.1.2 偏向锁
当MarkWord的是否偏向锁位为1,锁标志位为01时。对象锁为偏向锁状态
当偏向锁开启状态时,一个线程需要获取对象锁时,需要将Markword的偏向线程id位修改为自己的线程id。修改方式为CAS操作。当有其他线程访问此对象锁时,偏向锁升级为轻量级锁。
偏向锁的释放:
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁释放后就会升级为轻量级锁。
3.1.1.3 轻量级锁
当MarkWord的锁标志位为01时。对象锁为轻量级状态
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁,如果自旋一定次数(默认为10,可以通过参数-XX:PreBlockSpin来调整。--XX:+UseSpinning参数来开启自旋(JDK1.6之前默认关闭自旋))后依旧获取不到锁,轻量级锁会膨胀为重量级锁,且当前自旋的线程会阻塞。很多博客对这里说得含糊不清,需要注意。
轻量级锁中,某进程cas失败后自旋的意义是为了减少线程从用户态到内核态的上下文切换。CPU对这两种状态的切换比较耗时
补充:
自适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
3.1.1.4 重量级锁
当MarkWord的锁标志位为10时。对象锁为重量级状态,对象锁进入重量级状态后,将采用monitor的方式加锁和释放锁。
3.1.1.4.1 Monitor
对象锁在重量级状态时,是通过进入、退出 对象监视器(Monitor) 来实现对方法、代码块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。
互斥锁(Mutex Lock)
一种常见的线程同步机制,用于保护共享资源在多线程环境下的互斥访问。它提供了两个基本操作:加锁(Lock)和解锁(Unlock)。
互斥锁的原理和实现方式可以有多种,常见的实现包括使用原子操作、互斥变量、硬件指令等。下面是一个常见的互斥锁实现原理的简要分析:
原子操作实现:原子操作是一种不可中断的操作,能够保证在多线程环境下的原子性。互斥锁的实现中,常用的原子操作是比较并交换(Compare and Swap,CAS)操作。具体实现中,互斥锁内部维护一个标志位,用于表示锁的状态。加锁操作通过原子的CAS操作将标志位从未锁定状态修改为锁定状态,如果修改成功则表示获取锁成功,否则需要重试。解锁操作将标志位恢复为未锁定状态。
互斥变量实现:互斥变量是一种特殊的变量,它具有原子性操作和线程同步的特性。互斥锁的实现中,互斥变量被用作一个标志位,用于表示锁的状态。加锁操作通过原子的测试和设置操作来获取互斥变量的值,如果互斥变量的值为未锁定状态,则将其设置为锁定状态,表示获取锁成功。解锁操作将互斥变量的值恢复为未锁定状态。
硬件指令实现:一些处理器架构提供了特定的硬件指令来支持互斥锁的实现。这些指令通常能够在单个指令级别上执行锁的加锁和解锁操作,具有较高的性能和效率。这些硬件指令可以保证锁的操作是原子的,从而实现线程的同步和互斥访问。
代码块的加锁:
字节码的入口和出口分别有
monitorenter
和monitorexit
指令。当执行monitorenter
指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit
指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。大部分博客都是如是说,但是这里不准确,对象头中存放的是指向重量级锁的指针,也就是之前提到的每一个对象可以关联的那个ObjectMonitor对象。
方法的加锁:
字节码中是添加
ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
虽然字节码的标识不一样,但是都是会获取Monitor对象去做操作。
monitor的结构:
刚开始 Monitor 中 Owner 为 null
当 Thread-2 执行 synchronized(obj) 就会将Monitor的所有者 Owner 置为Thread-2 ,Monitor中只能有一个Owner
在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
Thread-2执行完同步代码块中的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的
图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,在wait-notify中
3.1.2 锁升级
锁升级是在线程获取对象锁时,对象锁状态的一种切换。对象锁的状态也实时影响着获取对象锁的线程的状态,是否阻塞等。
手动开启或关闭偏向锁,你可以使用
-XX:+UseBiasedLocking
或-XX:-UseBiasedLocking
参数来控制。需要注意的:
开启偏向锁的时候,对象锁只有 偏向锁->轻量级锁->重量级锁的升级流程。
关闭偏向锁的时候,对象锁只有 无锁->轻量级锁->重量级锁的升级流程。
3.1.3 锁消除
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?
所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?
我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。
逃逸分析:
Java - 深入理解Java中的逃逸分析_java doescapeanalysis-CSDN博客
3.1.4 锁粗化
就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
3.2 对象头
对象有三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
3.2.1 对象头的结构
对象头由三部分组成:MarkWord,指向类的指针,数组长度(只有数组对象才有)
3.2.1.1 MarkWord
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
Mark Word在不同的锁状态下存储的内容不同,在64位JVM中是这么存的:
PS:这里分代年龄是4位bit,也说明了为什么分代年龄最大是15。
3.2.1.1 指向类的指针(class pointer)
在看此部分时候,需要提前了解,类,实例对象,类的class对象的区别,是三个东西。
即指向方法区的instanceKlass实例 (虚拟机通过这个指针来确定这个对象是哪个类的实例。)
上图是JDK1.6的状态。
1.7和1.8中class的实例对象是放在堆中了
具体可查看这篇博客,类的class实例对象存放位置:
https://www.cnblogs.com/xy-nb/p/6773051.html
参考博客:
Java的对象头和对象组成详解_hotspot虚拟机中java对象结构的图示-CSDN博客
https://www.cnblogs.com/thiaoqueen/p/9314745.html
开启偏向锁一定性能更好吗?_jvm偏向锁导致性能问题-CSDN博客
Java - 深入理解Java中的逃逸分析_java doescapeanalysis-CSDN博客
Synchronized 关键字原理-CSDN博客
synchronized详解-CSDN博客