Wilder's Blog.

聊聊Synchronized和对象模型

字数统计: 2.6k阅读时长: 9 min
2018/11/30 Share

聊聊Synchronized和对象模型

Synchronized 的实现原理

看一下下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13

public class SynchronizedTest {

public synchronized void doSth(){
System.out.println("Hello World");
}

public void doSth1(){
synchronized (SynchronizedTest.class){
System.out.println("Hello World");
}
}
}

使用 javap 反编译以上代码,结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return

我们可以注意到两组关键字:
1、ACC_SYNCHRONIZED —— 用于同步方法
2、monitorenter、monitorexit —— 用于同步代码块

ACC_SYNCHRONIZED

当某个线程要访问某个方法的时候,会先检查方法是否有ACC_SYNCHRONIZED关键字,如果有设置则需要获取监视器锁,只有获得了监视器锁之后才能够执行方法中的
内容,方法执行完之后锁将会释放。如果线程拿不到这个方法的锁,则会被阻塞,直到获得锁才会继续执行。如果一个方法执行过程中出现了异常,而且对异常没有什么处理
,那么在异常被抛到方法外面之前监视器锁将会被自动释放。

monitorenter and  monitorexit

这个关键字用于同步代码块,当线程运行时发现有monitorenter关键字的时候,就意味着加锁,发现monitorexit关键字的时候就意味着解锁。每个对象维护者一个
记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得monitorenter后,计数器自增变为1,同一个线程再次获得该对象锁的时候,计数器再次自增。
当同一个线程遇到enterexit的时候释放锁(计数器减1),当计数器减为0的时候其它线程才可以获得锁,执行下面的代码块。

什么是Monitor

无论是同步方法还是同步代码块,无论 ACC_SYNCHRONIZED 还是 monitorenter 、monitorexit都是基于Monitor实现的,到底什么是Monitor。

管程

我们知道操作系统中进程的同步方式有信号量机制,但是对于信号量来说,每个要访问临界资源的进程都必须自备同步操作 wait(S) 和 signal(S)。这就是大量的同步操作分散在各个进程中,对系统的管理带来了麻烦。

管程的说明(源于《计算机操作系统(第四版)》):系统中的各种硬件资源和软件资源均可用数据结构抽象地描述其资源特性,而忽略它们的内部结构和实现细节。因此,可以利用共享数据结构抽象地表示系统中的共享资源,并且将对该共享数据结构实施的特定操作定义为一组过程。进程对共享资源的申请、释放和其他操作必须通过这组过程,间接地对共享数据结构实现操作。对于请求访问共享资源的诸多并发进程,可以根据资源的情况接受或阻塞,确保每次仅有一个进程进入管程,执行这组过程,使用共享资源,达到对共享资源所有访问的统一管理,有效地实现进程互斥。

管程有四部分组成:

  • 管程的名称
  • 局部于管程的共享数据结构说明
  • 对该数据结构进行操作的一组过程
  • 对局部于管程的共享数据设置初始值的语句

我自己对管程的理解是这样子的:把管程看做一个对象,对象里面包含了共享资源以及对共享资源的一系列操作,同时管程维护着对应的队列,包括阻塞队列等待队列,当进程进入管程时先进入阻塞队列,当管程内部共享资源没有进程执行时,从阻塞队列中拿出一个进程进入管程内部,当调用了wait进行等待时,进程将进入等待队列,然后从阻塞队列中拿出另一个进程进入管程内部。

Java线程同步相关的Monitor

并发编程中,Java提供了同步机制、互斥锁机制,这个机制的保障来源于监视器锁Monitor,每个对象都拥有自己的监视器锁Monitor。

Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:

  • 对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
  • 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。

参考博客对于Monitor的举例:

Monitor的理解

监视器的实现

Java虚拟机中,Monitor是基于C++实现的,由ObjectMonitor实现的,主要数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

ObjectMonitor中有几个关键属性:

1
2
3
4
5
_ownerL: 指向拥有ObjectMonitor对象的线程
_WaitSet: 存放处于等待的线程的队列
_EntryList:存放处于阻塞状态的线程的队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数

synchronized加锁的时候,会调用objectMonitor的enter方法,解锁时会调用exit方法,这种锁被称为重量级锁,说它重的原因是:Java 的线程是映射到操作系统线程之上的。如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的getset方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操纵。

Java对象模型

Java的对象模型包括:对象头、实例数据和对齐填充。其中对象头包括了锁状态标志、线程持有的锁等标志。Java对象在 HotSpot 虚拟机中是用了一个 OOP-Klass Model来进行表示。其中OOP是一个普通对象指针,而Klass是用来描述实例的具体类型。我是这么理解的,OOP表示的是这个类new出来的实例对象,Klass表示的是这个类 。OOP-Klass 模型分为 OOP 框架和 Klass 框架。

OOP

OOP体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

//定义了oops共同基类
typedef class oopDesc* oop;
//表示一个Java类型实例
typedef class instanceOopDesc* instanceOop;
//表示一个Java方法
typedef class methodOopDesc* methodOop;
//表示一个Java方法中的不变信息
typedef class constMethodOopDesc* constMethodOop;
//记录性能信息的数据结构
typedef class methodDataOopDesc* methodDataOop;
//定义了数组OOPS的抽象基类
typedef class arrayOopDesc* arrayOop;
//表示持有一个OOPS数组
typedef class objArrayOopDesc* objArrayOop;
//表示容纳基本类型的数组
typedef class typeArrayOopDesc* typeArrayOop;
//表示在Class文件中描述的常量池
typedef class constantPoolOopDesc* constantPoolOop;
//常量池告诉缓存
typedef class constantPoolCacheOopDesc* constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class klassOopDesc* klassOop;
//表示对象头
typedef class markOopDesc* markOop;

其中oopDesc是所有OOPS类的共同基本类型,instanceOopDesc代表一个类实例,arrayOopDesc表示数组。当我们使用new创建一个对象实例的时候,虚拟机就会创建一个instanceOopDesc来表示这个实例对象。

1
2
3
4
5
6
7
8
9
10

class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
}

我们可以看到oopDesc包含两个方面的数据,其中一个是 _mark,它表示对象头,另一个是 _metadata,他是一个共用体,这个字段被称为元数据指针。指向这个类的实例,也就是Klass对象的指针。

Klass

Klass体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//klassOop的一部分,用来描述语言层的类型
class Klass;
//在虚拟机层面描述一个Java类
class instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class instanceRefKlass;
//表示methodOop的Klass
class methodKlass;
//表示constMethodOop的Klass
class constMethodKlass;
//表示methodDataOop的Klass
class methodDataKlass;
//最为klass链的端点,klassKlass的Klass就是它自身
class klassKlass;
//表示instanceKlass的Klass
class instanceKlassKlass;
//表示arrayKlass的Klass
class arrayKlassKlass;
//表示objArrayKlass的Klass
class objArrayKlassKlass;
//表示typeArrayKlass的Klass
class typeArrayKlassKlass;
//表示array类型的抽象基类
class arrayKlass;
//表示objArrayOop的Klass
class objArrayKlass;
//表示typeArrayOop的Klass
class typeArrayKlass;
//表示constantPoolOop的Klass
class constantPoolKlass;
//表示constantPoolCacheOop的Klass
class constantPoolCacheKlass;

Klass模型和OOP模型类似,其中Klass也是其他类型Klass类型的父类。我们前面说到OOP的_metedata共用体中有两个指针,都指向了该类对应的类Klass对象——instanceKlass。

我们来看看isntanceKlass的内部结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//类拥有的方法列表
objArrayOop _methods;
//描述方法顺序
typeArrayOop _method_ordering;
//实现的接口
objArrayOop _local_interfaces;
//继承的接口
objArrayOop _transitive_interfaces;
//域
typeArrayOop _fields;
//常量
constantPoolOop _constants;
//类加载器
oop _class_loader;
//protected域
oop _protection_domain;

内部结构几乎包含了一个类应该有的内容:实现和继承的接口,方法列表等信息。

在JVM中,对象在内存中的基本存在形式是oop。那么对象所输的类实际上也是一个对象,也就是说Klass实际上也是一个对象,因此它们实际上会被组织成一种oop,叫做KlassOop,这个对象也有一个对应的类来进行描述,叫做KlassKlass,也是Klass的一个子类。KlassKlass作为oop的Klass链的端点。

oop-Klass模型
最后我们通过一个简单的样例代码来了解一下模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class Model
{
public static int a = 1;
public int b;

public Model(int b) {
this.b = b;
}
}

public static void main(String[] args) {
int c = 10;
Model modelA = new Model(2);
Model modelB = new Model(3);

在这里插入图片描述

CATALOG
  1. 1. 聊聊Synchronized和对象模型
    1. 1.1. Synchronized 的实现原理
      1. 1.1.1. ACC_SYNCHRONIZED
      2. 1.1.2. monitorenter and  monitorexit
    2. 1.2. 什么是Monitor
      1. 1.2.1. 管程
      2. 1.2.2. Java线程同步相关的Monitor
      3. 1.2.3. 监视器的实现
    3. 1.3. Java对象模型
      1. 1.3.1. OOP
      2. 1.3.2. Klass