聊聊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
31public 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的举例:
监视器的实现
Java虚拟机中,Monitor是基于C++实现的,由ObjectMonitor实现的,主要数据结构如下:
1 | ObjectMonitor() { |
ObjectMonitor中有几个关键属性:
1 | _ownerL: 指向拥有ObjectMonitor对象的线程 |
synchronized加锁的时候,会调用objectMonitor的enter方法,解锁时会调用exit方法,这种锁被称为重量级锁,说它重的原因是:Java 的线程是映射到操作系统线程之上的。如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的get 或set方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操纵。
Java对象模型
Java的对象模型包括:对象头、实例数据和对齐填充。其中对象头包括了锁状态标志、线程持有的锁等标志。Java对象在 HotSpot 虚拟机中是用了一个 OOP-Klass Model来进行表示。其中OOP是一个普通对象指针,而Klass是用来描述实例的具体类型。我是这么理解的,OOP表示的是这个类new出来的实例对象,Klass表示的是这个类 。OOP-Klass 模型分为 OOP 框架和 Klass 框架。
OOP
OOP体系
1 |
|
其中oopDesc是所有OOPS类的共同基本类型,instanceOopDesc代表一个类实例,arrayOopDesc表示数组。当我们使用new创建一个对象实例的时候,虚拟机就会创建一个instanceOopDesc来表示这个实例对象。
1 |
|
我们可以看到oopDesc包含两个方面的数据,其中一个是 _mark,它表示对象头,另一个是 _metadata,他是一个共用体,这个字段被称为元数据指针。指向这个类的实例,也就是Klass对象的指针。
Klass
Klass体系
1 | //klassOop的一部分,用来描述语言层的类型 |
Klass模型和OOP模型类似,其中Klass也是其他类型Klass类型的父类。我们前面说到OOP的_metedata共用体中有两个指针,都指向了该类对应的类Klass对象——instanceKlass。
我们来看看isntanceKlass的内部结构:
1 | //类拥有的方法列表 |
内部结构几乎包含了一个类应该有的内容:实现和继承的接口,方法列表等信息。
在JVM中,对象在内存中的基本存在形式是oop。那么对象所输的类实际上也是一个对象,也就是说Klass实际上也是一个对象,因此它们实际上会被组织成一种oop,叫做KlassOop,这个对象也有一个对应的类来进行描述,叫做KlassKlass,也是Klass的一个子类。KlassKlass作为oop的Klass链的端点。

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