1.1 实现基础
synchronized实现同步的基础:java中的每一个对象都可以作为锁(表现为3种形式)。
- 对于普通同步方法,锁是当前实例对象(this指针)
- 对于静态同步方法,锁是当前类的Class对象(xxx.class)
- 对于同步方法块,锁是synchronized括号里指定的对象(synchronized(obj){})
1.2 对象头
synchronized使用的锁对象是存储在Java对象头里的。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
- 对象头
- 实例数据
- 对齐填充
非数组对象头分为两部分,数组对象分为三个部分(每个部分占1个字宽):
- 第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持久的锁、偏向线程的ID等。官方称为Mark Word
- 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。
注意:无锁状态时是JVM默认状态的对象头结构,在运行期间,对象头结构会根据锁的不同发生下列结构变化
1.3 实现原理
JVM规范中描述了synchronized实现原理:JVM基于进入和退出monitor对象来实现方法的同步和代码块同步,但两者的实现细节不一样。
- 代码块同步是使用monitorenter和monitorexit指令实现的
- monitorenter指令在编译后插入到同步代码块的开始位置
- monitorexit指令插入到方法结束处和异常处
- 两个指令必须成对出现一一对应
- 方法同步使用另一种方式,JVM规范中没有细说,但是它同样可以使用这两个指令实现
// 通过编译.class文件进行对比可以发现,同步代码块是使用monitorenter和monitorexit指令实现,
// 而同步方法是在方法标记位上添加ACC_SYNCHRONIZED来实现
public class TestSynchronized {
public static void main(String[] args){
test01();
}
public static void test01(){
synchronized (TestSynchronized.class){
System.out.println("test method");
}
}
public synchronized static void test02(){
System.out.println("test method");
}
}
任何对象都有一个monitor与之关联,当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,会尝试获取对象锁对应的monitor的所有权,即尝试获得对象的锁
1.4 锁的升级与对比
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。在1.6中,锁一共有4中状态(由低到高,锁可以升级但不能降级):
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
1.偏向锁
偏向锁意义:据调查,大多数情况下,锁总是被同一线程调用,所以就出现了偏向锁。
偏向锁总是偏向于第一次获取锁的线程,一般无竞争状态时使用的就是偏向锁,可以使用-XX:+/-UseBiasedLocking参数启用/关闭偏向锁,默认是开启的(线程数多的时候,适合关闭偏向锁,因为竞争激烈,锁会升级,关闭后可以省略升级过程,直接使用轻量级锁)。
偏向锁加锁过程:
- 执行到monitorenter时,首先检查所标志是否为01(01表示未上锁)
- 其次检查偏向锁位是否为0(0表示无锁,1表示当前已是偏向锁)
- 如果检查成功,通过CAS操作,将Mark Word中的线程ID设置为自己的线程ID,然后将偏向锁位设置为1
- 如果第二次仍是该线程进入同步区,那么不再需要执行第三步
- 如果第二次是其他线程进入,那么锁升级
偏向锁升级为轻量级锁:
- 竞争激烈,多个线程同时争锁,锁升级为轻量级锁
- 第一个线程进入后设置为偏向锁,第二次再来的线程不是该线程而是其他线程,锁升级为轻量级锁
关闭偏向锁
偏向锁是默认启用的,它会在程序启动几秒后才激活,可以通过JVM参数:-XX:BiasedLockingStartupDelay=0关闭延迟。也可以使用参数:-XX:UserBiasedLocking=false关闭偏向锁
2.轻量级锁
加锁过程:
- 当某个线程想要占用这把锁的时候,它会首先在自己的栈(帧)中创建一个锁记录(LockRecord)
- 然后将Mark Word信息复制(hashCode、分代年龄等)到LockRecord中
- 用CAS操作将Mark Word替换掉LR指针,即将Mark Word中除了标志位外的部分替换成一个指针,这个指针指向自己的LockRecord
- 哪个线程替换成功,哪个线程就成功得到了锁
轻量级锁自旋次数超过10(任意一个线程),或者正在自旋的线程超过CPU核数一半,那么轻量级锁升级为重量级锁,可以通过-XX:+PreBlockSpin指定自旋次数
3.重量级锁
因为需要阻塞,并且需要向操作系统级别申请,所以说是重量级。重量级锁是操作系统来决定由哪个线程来获得锁的:
- 生成或者复用monitor对象
- Mark Word不再指向LR,而是指向monitor对象
- 线程阻塞,进入_EntryList排队,由操作系统决定唤醒谁
- 操作系统唤醒线程后该线程的Mark Word替换为monitor pointer,然后执行
monitor对象
monitor可以理解为一个操作系统级别的互斥变量,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。
在hotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的
Java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。
推荐一篇关于锁只是的博客,原文链接 :https://blog.csdn.net/lengxiao1993/article/details/81568130