java虚拟机中的synchronized是基于进入和退出monitor对象实现的,同步分为显式同步和隐式同步,同步代码块代表着显式同步,指的是有明确的monitorenter和monitorexit指令。同步方法代表着隐式同步,同步方法是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的。
对象在堆内存中存在三块区域,包括对象头,实例变量和对齐填充
实例变量,存放着类的属性的相关信息,包括父类的属性信息,如果是数组对象还包括数组的长度
填充数据,仅仅是为了字节对齐,因为java虚拟机要求对象起始地址必须是8字节的整数倍
对象中的对象头是实现synchronized的基础,synchronized使用的锁对象就是存储在java对象中的对象头中,java虚拟机使用2个字来存储对象头,如果对象是数组,使用三个字存储对象头,多出来的一个字用于存放数组的长度。
对象头中的主要结构是MarkWord 和Class Metadata Address组成
其中:
MarkWord中存储对象的hashCode、分代年龄、GC标志、锁信息
MetadataAddress(元数据地址)
jvm通过MetadataAddress这个指针确定该对象是哪一个类的实例
对象的头信息是与对象自身定义的没有关系的额外存储,MarkWord默认情况下存放着对象的hashcode、分代年龄,锁标记位、锁信息等等,考虑对象头信息占用的内存会影响jvm的空间,Markword被设计称为变化的数据结构,它会根据对象本身的状态复用自己的存储空间,如下看MarkWord可能存在的存储结构为:
重量级锁就是synchronized锁,锁的标记位为10,其中指针指向monitor对象的起始地址。每一个对象都存在一个monitor与之相关联,monitor对象可以与对象一起创建销毁或者当线程试图获取对象锁的时候自动生成,monitor被线程持有之后就处于锁定的状态java虚拟机中monitor是有ObjectMonitor实现的,其主要的数据结构为:
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
monitor对象中有内部有两个队列,一个用来保存一个ObjectWaiter对象列表(每个等待这个锁的线程都会被封装成为ObjectWaiter对象),owner指向的是持有持有monitor对象的线程,当多个线程同时访问一段代码的时候,首先会进入_Entry_list集合,当线程获取到对象的Monitor后进入,会进入 _owner区域并把monitor中的owner设置为当前线程,同时monitor中的计数器count加1,如果线程调用wait()方法,将会释放当前持有的monitor,owner设置为null,count减1,同时该线程进入WaitSet集合中等待被唤醒,如果当前线程执行完毕也将会释放monitor锁,owner变为null,count减1
总结上面的内容可以知道为什么所有的对象都可以作为锁,另外synchronized是怎么获取锁的,表示锁的monitor对象是什么,monitor对象存在每个java对象的对象头中(MarkWord结构中存储了指向monitor对象的指针),也就是为什么notify,notifyall,wait方法都存在Object对象中的原因
synchronized在字节码层面的实现原理
**
**
如下类中包含了代码块
public class SyncCodeBlock {
public int i;
public void syncTask(){
//同步代码库
synchronized (this){
i++;
}
}
}
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
Last modified 2017-6-2; size 426 bytes
MD5 checksum c80bc322c87b312de760942820b4fed5
Compiled from "SyncCodeBlock.java"
public class com.zejian.concurrencys.SyncCodeBlock
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//........省略常量池中数据
//构造函数
public com.zejian.concurrencys.SyncCodeBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 7: 0
//===========主要看看syncTask方法实现================
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此处,进入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此处,退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此处,退出同步方法
22: aload_2
23: athrow
24: return
Exception table:
//省略其他字节码.......
}
SourceFile: "SyncCodeBlock.java"
上面已经说过synchronized修饰代码块的方式属于显式同步,查看字节码文件之后会发现,synchronized修饰的同步代码块中,从monitorenter进入同步代码块,从monitorexit退出同步代码块。当执行monitorenter指令的时候,当前线程将会试图获取对象头中指向的monitor对象,当monitor对象结构中的conut为0的时候,线程可以成功的获取到monitor,并将count的值设置为1,表示锁获取成功如果当前线程中已经拥有了monitor,那么可以重新进入这个monitor,此时count再加1。如果monitor已经被其他线程持有了,那么当前线程就会被阻塞,只当正在执行的线程执行完毕,执行完monitorexit指令,这时候,owner变为null,count为0,其他线程将有机会持有monitor对象,编译器将会确保无论每一个执行过monitorentor的方法都有一个monitorexit指令与其相对应,不论这个方法时候正常结束,即使是异常也会执行monitorexit指令。这就是synchtonized修饰的同步代码块中为什么出现两个monitorexit指令的原因
如下代码是使用synchronized修饰的方法:
public class SyncMethod {
\
public int i;
public synchronized void syncTask(){
i++;
}
}
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
Last modified 2017-6-2; size 308 bytes
MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool;
//省略没必要的字节码
//==================syncTask方法======================
public synchronized void syncTask();
descriptor: ()V
//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
}
SourceFile: "SyncMethod.java"
之前有说过同步有隐式同步和显式同步,synchronized修饰的同步代码块是显式同步,而synchronized修饰的方法是隐式同步,是通过方法调用指令读取运行时常量池中的ACC_SYNCHRONIZED标志来区分这个方法是否是同步方法。在方法调用的时候会检查方法的ACC_SYNCHRONIZED访问标志是否被设置了,如果被设置了,执行线程将会现持有monitor当方法调用的时候将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置了,如果被设置了,表明是一个同步方法,这个时候,执行线程将会先持有monitor对象(尝试持有,可能没有持有成功),然后再访问执行方法,最后再方法完成的时候,无论是正常完成还是出现异常都会释放monitor。在方法执行期间其他任何一个线程都不能获得同一个monitor对象。如果在方法执行的时候出现了异常并且方法内部无法处理这个异常的时候,这个同步方法持有的monitor将会在异常抛到方法为的时候自动释放
总结synchronized修饰方法可以看出,jvm通过判断ACC_SYNCHRONIZED访问标志来判别一个方法是否是同步方法,进而获取monitor对象,在早期synchronized是重量锁效率低,因为monitor是依赖底层操作系统来实现的,而操作系统实现线程之间的转换需要从用户太转换到核心态,这个转换要浪费很多时间,后来从jvm层面对synchronized有了很大的优化,为了减少获得锁或者是释放锁的性能消耗,引入了轻量级锁和偏向锁,所以现在的synchronized还可以。