JVM支持方法级和方法内部一段指令序列的同步,都用同步锁(monitor)实现
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入临界区,同时它还可以保证共享变量的内存可见性
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础
1. 普通同步方法,锁是当前实例对象
2. 静态同步方法,锁是当前类的class对象
3. 同步方法块,锁是括号里面的对象
当一个线程访问同步代码块
首先得到锁才能执行同步代码
当退出或者抛异常时必须释放锁
如何来实现这个机制呢?
实现原理
看一段简单的代码
生成的class文件信息
JVM的同步(synchronization)是monitor的进入和退出实现的,无论是显式同步(有明确的monitorenter和monitorexit指令),还是隐式同步(无需通过字节码指令控制,依赖方法调用和返回指令实现)
Java中,同步用的最多的可能就是经synchronized修饰的同步方法
同步方法并不是用monitorenter和monitorexit实现的,而是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED
标志来隐式实现的0
.monitorenter
和monitorexit
指令用于编译synchronized同步0.语句块
JVM从方法常量池中的方法表结构中的ACC_SYNCHRONIZED
访问标志区区分是否为同步方法
调用时,调用指令将会检查方法的ACC_SYNCHRONIZED
是否设置,是则执行线程将先获得同步锁,然后执行方法,最后完成方法时释放同步锁
执行期间,其他线程都无法获得同一个锁,若运行期间抛异常并且方法内部无法处理时,所持有的锁将在抛异常到同步方法之外时自动释放
同步代码块
monitorenter
插入到同步代码块的开始位置monitorexit
插入到结束位置
JVM需要保证每一个monitorenter
都有一个monitorexit
对应
任何对象都有一个monitor
与之关联,且当一个monitor
被持有之后,他将处于锁定状态
线程执行到monitorenter
时,将会尝试获取对象的monitor
所有权,即尝试获取对象的锁
同步方法
synchronized方法会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令
在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,
而是在Class文件的方法表中将该方法的access_flags
字段中的synchronized
标志位 置1,表该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象
深入之前我们需要了解两个重要的概念:Java对象头,Monitor。
Java对象头
synchronized
用的锁存在Java对象头
对象头主要包括
Klass Pointer(类型指针)
对象指向它的类元数据的指针
JVM通过该指针确定该对象是何类的实例Mark Word(标记字段)
存储对象的运行时数据
是实现轻量级锁和偏向锁的关键
Mark Word
存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等
Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度
Java对象头的存储结构(32位VM)
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):
Monitor
可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。
和万物皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁
Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。
每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下
Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数
Nest:用来实现重入锁的计数
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。
作者:芥末无疆sss
链接:https://www.jianshu.com/p/903d9f236dcc
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。