volatile的实现原理
当有volatile变量修饰的共享变量进行写操作的时候会多出一行有Lock前缀指令的汇编代码。
Lock前缀的指令在多核处理器下会发生两件事情:
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
原因:多处理器下的缓存一致性协议(MESI),每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址的数据被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
synchronized的实现原理
JVM基于进入和退出Monitor对象来实现代码块同步和方法同步,但两者的实现细节不一样。
1)同步代码块
使用monitorenter和monitorexit指令实现。
monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit是插入到结束位置。
JVM保证每一个monitorenter必须有对应的monitorexit。
任何对象都有一个Monitor与之关联,当且有一个Monitor被持有后,它将处于锁定状态。
线程执行到monitorenter指令时,将会尝试获取对象所对应的Monitor所有权,即尝试获取对象的锁。
2)同步方法
在Class文件的方法表中将该方法的access_flags字段中设置ACC_SYNCHRONIZED标志来实现。
对象在堆内存中的布局分为三块区域:对象头、实例变量和填充数据,synchronized用的锁是存在Java对象头里的。
对象头中包括:
- Mark Word,存储对象的hashCode、分代年龄和锁标记位,占1字宽(32位JVM1字宽为4字节)
- Class Metadata Address,存储到对象类型数据的指针 ,占1字宽
- Array length,如果对象是数组,表示数组的长度,占1字宽
特别的,对象头信息是与对象自身定义的数据无关的额外存储成本,所以为了节省空间,JVM采用了空间复用,即当对象处于不同状态的时候,Mark Word存储的内容是不一样的。
每一个被锁住的对象都会和一个Monitor关联(对象头的MarkWord中存储了指向Monitor起始地址的指针),同时Monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
在Java中是ObjectMonitor(JVM源码中C++实现)来实现Monitor。
ObjectMonitor中几个关键字段的含义:
_count:记录owner线程获取锁的次数,这也决定了synchronized是可重入的。
_owner:指向拥有该对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放等待锁而被block的线程队列
- 想要获取Monitor的线程先进入Monitor的_EntryList队列阻塞等待,即遇到synchronized关键字时。
- 如果Monitor的_owner为空,则从队列中移出并赋值给_owner。
- 如果在程序里调用了wait方法,则该线程进入_WaitSet队列。注意wait方法会释放Monitor锁,即将_owner赋值为null并进入_WaitSet队列阻塞等待,这时其他在_EntryList中的线程就可以获取锁了。
- 当程序里其他线程调用了notify/notifyAll方法时,就会唤醒_WaitSet中的某个线程,这个线程就会再次尝试获取Monitor锁,如果成功,则就会成为Monitor的owner。
- 当程序里遇到synchronized关键字的作用范围结束时,就会将Monitor的owner设为null,退出。
Java中的wait/notify/notifyAll方法底层都是调用了ObjectMonitor的方法。