synchronized的实现原理

今天复习了synchronized的实现原理,在这里进行总结一下。也是对自己的检查
一、synchronized是java中同步的一个关键字它是一个重量级锁,它可以修饰方法和代码块
1. synchronized修饰非静态方法时,它锁住的是当前实例。
2. synchronized修饰静态方法时,它锁住的是整个类(.class)。
3. synchronized修饰代码块时,它锁住的是synchronized后面的括号中的对象,这个对象可以是某个对象,也可以是某个类(.class)。
注意:一个线程访问一个类的static synchronized方法时,其他线程可以访问该类的非static synchronized的方法。原因是两个线程所获得的锁不同,前者是锁住的是这个类,而后者锁着的是这个实例对象,因此不存在互斥关系。

二、synchronize的底层实现原理
在说synchronized实现原理之前必须介绍一下,对象头和monitor(监视器)
对象是存储在堆中的,存储的内容分为三个部分:对象头、实例变量、填充字节
1. 对象头:对象头主要是由Mark Word 和 Klass Point(类元指针),类元指针是用来指向类元数据的指针,JVM通过这个指针可以知道这个对象是哪个类的实例;而Mark Word中存储的则是用于自身的运行时数据,和synchronized的实现原理关系很大。如果对象是数组,则对象头占3个字节,如果是非数组则占2个字节,原因:如果是数组要记录数组的长度。
2. 先说实例变量:所谓实例变量就是存储对象的属性信息的,包括从父类继承来的属性。并且按照4字节对齐
3. 填充字节:顾名思义它的作用就是用来填充字节的。由于JVM规定对象的起始地址必须是8字节的整数倍。

从上面我们知道synchronized无论是修饰方法还是代码块,都是通过修饰对象的得锁来实现同步的。那么synchronized的锁对象是存在哪里的?答案就是存在对象头中的 Mark Word内的。那么接下来看一下Mark Word中是怎样存储的呢?
下面主要介绍32位虚拟机中的存储,64位的原理相同。
在32位虚拟机中,Mark Word在没有获取锁之前存储的内容及状态图:

在获取轻量级锁或者重量级锁状态下,Mark Word 存储的内容以及状态图如下:(它会将之前存储HashCode、分代年龄、偏向锁标志的位置进行替换)



那么什么是monitor呢?
Monitor:可以理解为一个监视器,在java中每个对象和类的内部都是和一个监视器monitor相关联的,为了实现监视器的排他性监视能力,JVM为每个对象和类都关联了一个监视器。并且任何对象都只有一个monitor与之对应,当这个对象的monitor被线程持有之后,他将进入锁定状态,其他线程无法访问。换句话来说就是:锁住了一个对象,就是获得了该对象的monitor。monitor对象实际是由底层ObjectMonitor(C++)对象实现的。主要介绍他的来历,在JVM的顶级基类oopDesc中有markOop类的子对象_mark 通过调用_mark 对象的monitor()这个方法,通过这个方法可以获得该对象的ObjectMonitor监视器对象。
class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;    //JVM的顶层基类oopDesc类中,有markOop类的子对象 _mark
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  // Fast access to barrier set.  Must be initialized.
  static BarrierSet* _bs;
}
那么再看看markOop类中的具体有什么?
class markOopDesc: public oopDesc {
 public:
  bool has_monitor() const {
    return ((value() & monitor_value) != 0);
  }
  ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
  }
}
markOopDesc这个类被定义在markOop.h文件中markOop类
也就是说java中每个对象中都寄生者一个监视器对象ObjectMonitor,它有如下几个字段:
ObjectMonitor() {
_recursions   = 0;    //锁的重入次数
_object       = NULL    //监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner        = NULL;    //指向持有ObjectMonitor对象的线程
_WaitSet      = NULL;    //处于wait状态的线程,会被加入到_WaitSet
_EntryList = NULL ;   //处于等待锁block状态的线程,会被加入到该列表
//...
}
	
	
	
	
	
下面举例介绍上面字段的含义,对一个synchronized修饰的方法(代码块)来说:
a、当多个线程访问该区域时,由于synchronized修饰只有一个会进入运行状态,其他线程都被阻塞此时其他线程进入blocking状态,这些线程都会被加入到_EntryList列表中去
b、获得该锁的线程即就是获得该对象的monitor的线程,进入运行状态,并且将monitor对象中_owner存储当前线程的地址。
c、当处于运行状态的线程调用了wait方法之后,那么当前线程释放monitor对象,进入等待状态。此时ObjectMonitor对象内_owner字段赋值为null,同时将该线程加入到_WaitSet列表中。直到有某个得到monitor对象的线程调用了notify或者notifyAll方法,则该线程才有机会从_WaitSet列表中移除,加入到_EntryList列表中等待获取monitor监视器锁。(注意:notify方法并不会释放对象锁,它会在方法结束时释放对象锁)
d、如果当前线程执行完毕,也会释放掉monitor监视器锁,此时ObjectMonitor对象的_owner字段重新赋null。


那么synchronized修饰的方法和synchronize修饰的代码块又是通过什么方式来获取monitor监视器锁呢?

A、synchronized修饰代码块时,通过在需要同步的代码块开始的位置插入一条monitorentry指令(底层实际是调用monitor()方法来获得的ObjectMonitor对象也就是monitor监视器锁)表示即将进入监视区域,需要获取monitor监视器锁,只有成功获取才可以执行代码块中的内容。在代码块的结束位置或者异常出现位置插入一条monitorexit指令表示退出监视区域,此时释放monitor监视器锁。为什么要插入两条monitorexit指令呢?原因是JVM要保证每一个monitorentry都要有一个monitorexit与它对应,所以就必须在两种退出监视区域的位置都要插入monitorexit,以保证无论哪种情况退出监视区域都可以释放monitor监视器锁。可以通过javap -v命令来查看构成java字节码的指令,下面看具体代码:
源代码为:
public class test{
 private int i=0;
 public void fun(){
  synchronized(this){
   System.out.println(i++);
  }
 }
}
 
使用javap命令得到构成class文件中字节码的指令:
public class test {
public test();
Code:
0: aload_0
1: invokespecial #1
4: aload_0
5: iconst_0
6: putfield      #2
9: return
public void fun();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter                //获取该monitor监视器锁,进入该监视区域
4: getstatic     #3
7: aload_0
8: dup
9: getfield      #2
12: dup_x1
13: iconst_1
14: iadd
15: putfield      #2
18: invokevirtual #4
21: aload_1
22: monitorexit                  //方法执行完毕时,释放monitor监视器锁
23: goto          31
26: astore_2
27: aload_1
28: monitorexit                 //异常发生时,释放monitor监视器锁
29: aload_2
30: athrow
31: return
Exception table:
from    to  target type
4    23    26   any
26    29    26   any
}

B、当synchronized修饰同步方法时,获取monitor监视器的方式与代码块有所差异。不再是通过插入monitorentry和monitorexit指令来获取和释放monitor监视器锁。而是在该方法表结构中设置ACC_SYNCHRONIZED标志。如果有线程执行到有ACC_SYNCHRONIZED标志的方法时,它会在执行方法之前去尝试获取monitor监视器锁。如果成功获取则方法正常执行,获取失败则说明该monitor监视器锁已经被其他线程获取,则当前线程进入阻塞状态等待monitor监视器锁被释放。可以通过javap -c命令来查看构成java字节码的指令,下面看具体代码:
源代码为:
public class test{
 private int i=0;
 public synchronized void fun(){
  System.out.println(i++);
 }
}
使用javap命令得到构成class文件中字节码的指令:
public class test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
{
public test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1
4: aload_0
5: iconst_0
6: putfield      #2
9: return
LineNumberTable:
line 1: 0
line 2: 4
public synchronized void fun();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED            //ACC_SYNCHRONIZED标志
Code:
stack=5, locals=1, args_size=1
0: getstatic     #3
3: aload_0
4: dup
5: getfield      #2
8: dup_x1
9: iconst_1
10: iadd
11: putfield      #2
14: invokevirtual #4
17: return
LineNumberTable:
line 4: 0
line 5: 17
}

那么synchronized的底层原理就总结到这了,下一篇详细总结JVM中对synchronzied锁的优化!

由于牛客网这个博客上java代码格式有问题,不能换行,因此就用文本直接贴出来了。


全部评论

相关推荐

昨天 13:29
已编辑
湖南铁道职业技术学院 后端
小红书 后端选手 n*16*1.18+签字费期权
点赞 评论 收藏
分享
09-27 00:29
东北大学 Java
伟大的麻辣烫:查看图片
阿里巴巴稳定性 75人发布 投递阿里巴巴等公司10个岗位
点赞 评论 收藏
分享
1 2 评论
分享
牛客网
牛客企业服务