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中

也就是说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锁的优化!

 

 

 

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值