Synchronized是Java中的一个关键字,可以作用于普通方法,静态方法,代码块,使得被修饰的资源在同一时间只能由一个线程来访问。是Java语言用来保证线程安全的一种锁。可是它仅仅知识一种锁吗?它和别的锁都有什么关系呢?今天让我们来聊聊Synchronized的底层原理。
Synchronized的理解我我准备从三个层面开始说起,java语言层面,字节码层面,以及jvm层面:
Java语言层面:
Synchronized会应用在一下三种场景当中:
-
普通方法:在普通方法中存在共享变量的操作,而该方法又存在多线程场景,那么我们一般会对该方法加上Synchronized关键字,这边举个例子
public class Test { public static void main(String[] args) { new Test().hello(); } //锁方法 public synchronized void hello(){ System.out.println("hello world"); } }我们使用javap-c可以查看Test类所对应的字节码文件如下:
//其余部分省略 public synchronized void hello(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String hello world 5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 16: 0 line 17: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this LTest;可以看到:flags中出现了两个属性,一个是ACC_PUNLIC,另外一个是ACC_SYNCHRONIZED。第一个属性是表示该类的访问类型为public,如果为ACC_PRIVATE就表示访问类型为private。第二个属性就表示该方法为同步方法,同一时间只允许一个线程对其进行操作。
-
代码块:在代码块中存在共享变量的操作,而该方法又存在多线程场景,那么我们一般会对该方法加上Synchronized关键字,这边举个例:
public class Test { public static void main(String[] args) { new Test().hello(); } public void hello(){ //锁同步代码块 synchronized (this){ System.out.println("hello world"); } } }用javap-c可以查看Test类所对应的字节码文件如下:
//其余部分省略 //这是加了锁的字节码 public void hello(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #6 // String hello world 9: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_1 13: monitorexit 14: goto 22 17: astore_2 18: aload_1 19: monitorexit 20: aload_2 21: athrow 22: return可以看到,在指令当中多了monitorenter,和 monitorexit指令,这两个指令的意思就是进入同步代码块,和出同步代码块的意思,中间的内容同一时间只能由一个线程访问。
-
静态方法:在静态方法中存在共享变量的操作,而该方法又存在多线程场景,那么我们一般会对该方法加上Synchronized关键字,这边举个例:
public class Test { public static void main(String[] args) { new Test().hello(); } public synchronized static void hello(){ System.out.println("hello world"); } }用javap-c可以查看Test类所对应的字节码文件如下:
//其余部分省略 public static synchronized void hello(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=2, locals=0, args_size=0 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String hello world 5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 16: 0 line 17: 8和普通方法相同,只不过多了static字段而已,但是其实还是有所不同,因为对于static的方法不是作用于对象头的,这一点后面我们再讲。
字节码层面:
其实字节码层面是基于两种命令来实现的,因为我们知道,jvm会将Java文件编译为自己可以识别的字节码文件,也就是class文件。可以理解为Java文件的执行,其实就是字节码指令的执行。除了几个比较特殊的存在,几乎所有Java当中的功能的最终实现,其实都是jvm对字节码指令的执行得到的结果。这里举几个例子,大家看看就好:
- 本地方法调用(Native Method Invocation): Java 允许通过使用
native关键字声明本地方法,这些方法的实现是用其他语言(如 C 或 C++)编写的。这些本地方法可以直接调用底层操作系统的功能或库。虽然本地方法使用字节码调用的方式,但它们涉及到与 Java 虚拟机外部环境的交互,因此不完全属于字节码执行范畴。 - JNI(Java Native Interface): JNI 是一种允许 Java 代码与本地代码(如 C 或 C++)交互的机制。通过 JNI,Java 代码可以调用本地方法,并且本地代码也可以回调 Java 方法。这涉及到更多的底层交互,不仅仅是字节码指令的执行。
- Java 虚拟机内部机制: Java 虚拟机实现了许多内部机制,如垃圾回收、类加载、字节码解释、即时编译等。这些机制在一定程度上超出了纯粹的字节码指令执行,涉及到虚拟机的内部逻辑和管理。
- Java 标准库之外的外部库和框架: Java 生态系统包含许多外部的库和框架,例如 Spring、Hibernate、Netty 等。这些库和框架提供了更高级别的抽象和功能,它们的实现可能涉及多种技术和机制,不仅仅局限于字节码执行。
当然Synchronized是属于字节码指令执行的结果,对应的两个指令分别为:monitorenter,monitorexit。当带有Synchronized关键字的文件反编译后我们会发现存在一个monitorenter指令和两个monitorexit指令。当执行到monitorenter指令后程序进入同步状态,此时只允许一个线程进入执行该代码块。等到monitorexit执行以后,才允许别的线程来执行。我们可以看到一般一个monitorenter后面会跟着两个monitorexit,第一个monitorexit是结束同步,第二个monitorexit的意思是保证程序可以退出,可以参考finally()。
jvm层面:
好了,到了今天的重点了,Synchronized它在底层究竟是怎么实现的呢。他其实是通过mark word(对象头)中的地址去找到一个叫做monitor的东西来实现的,要搞清楚这些东西,我们需要了解下mark word的结构,还有monitor这个东西具体是什么。
在我们聊对象头的结构之前,我们需要知道一个类的对象当中都包含了什么信息
在一个类被加载时,会为这个类生成一个专属的class对象。而这个类每次被实例化以后,都会为这个类生成一个实例对象,我们这里说的对象指的是后者。一个类的对象分为三个部分:对象头,实例变量,对其填充。
对象头:Mark word和一个指向类对象的指针。
实例变量:存放一些对象的基本信息,如果是普通类型数据的话就是一些值,如果是引用类型的话就是一个指向内存的指针。
对其填充:没有什么实际意义,因为jvm对象必须是8的整数倍,如果不满足这个条件的话这个字段会对其进行填补。
编辑
那Mark word又是个什么东西呢?
编辑
Mark word是jvm实现的一种可变的数据结构,里面包含了一个对象的hash码,gc年龄以及锁状态信息。为什么说它可变的,因为这个结构的前30位都可以用来表示不同的信息,后两位都是用来表示锁状态相关信息的。
因为Synchronized在jdk6以前,对于互斥量会直接加一个重量级的锁,即通过Mark word中monitor的地址去访问monitor,而monitor是由ObjectMonitor实现的,源码由c++实现,里面主要的属性如下图编辑
这些属性我们在这里不做过多注释,因为已经超出了Java语言的范畴,我们主要研究下jdk6以后的一些变动。
虽然Synchronized可以保证数据的安全性,可是当锁住整个共享资源以后,其他访问共享资源的线程再次访问会进入阻塞状态,而阻塞的操作是非常耗费系统资源的。
因为Java的线程是映射到操作系统的原生线程之上的,一个线程的阻塞和唤醒是需要操作系统介入完成的,这就牵扯到了用户态向内核态的转换,这种操作是非常耗费系统资源的。因为用户态和内核态都有自己专用的内存空间,和专属的寄存器等,用户态切换内核态需要传递许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。了解
- 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
- 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
由此可以大致推测出Synchronized实现的锁是一个极其笨重的锁,一点也不灵活。其实实际上也是这样的。不过在jdk1.6以后,Synchronized加入了一个锁升级,锁粗化,锁消除等几个过程,这解决了在简单的同步代码块中,Synchronized过于笨重的问题,使Synchronized变成了一个很“灵活”的锁。至于锁升级的过程下面我们继续研究。
编辑
这是mark word的结构,可以看到,其中不仅仅有有关monitor的重量级锁,还包含了偏向锁,轻量级锁这两个字段。没错,这就是jdk6以后对于Synchronized进行的优化操作,具体的优化过程我们开始解密:
偏向锁
- 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
- 如果CAS获取偏向锁失败,则表示有竞争。偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
- 执行同步代码。
。
轻量级锁
- 先会访问mark word的锁标志位,如果为01,那么就会在当前线程的栈帧当中简历一个名为锁记录的空间,如图所示
编辑
- 然后回拷贝mark word的信息到这个锁记录中。
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示:
编辑
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程
至于为什么要复制一份Mark word的信息到Lock Record,我在刚刚看的时候是有疑惑的,最后明白了是要和原Mark word中数据对比,因为期间如果有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。
上面在介绍Synchronized作用于静态方法的时候,锁的是类,就不是对象头了。对于类级别的锁(静态synchronized方法或代码块),情况略有不同。在Java虚拟机中,并没有为类的元数据(Class对象)分配对象头。因此,JVM不会直接使用对象头来实现对整个类的锁。相反,JVM在内部维护了一个用于类级别锁的数据结构。当一个线程尝试进入一个类级别的synchronized代码块或方法时,JVM会使用该类的元数据(Class对象)作为锁标识,而不是使用对象头。这个锁标识实际上是一个指向内部数据结构的引用,而不是实际的对象头。这使得可以在没有类实例的情况下锁定整个类,也就是类级别的锁。
这就是Synchronized的锁升级的过程
总结:本文我们从Java语言,字节码,jvm三个层面介绍了Synchronized的功能,作用场景,原理等一些内容,如有问题欢迎指正。