Java中,针对synchronized有无锁,偏向锁,轻量级锁,重量级锁4种锁状态
级别从低到高依次是:无锁 > 偏向锁 > 轻量级锁 > 重量级锁。
而且锁的状态只有升级,没有降级。
但在学习这4种锁状态前我们得先了解一下这些知识。
这就涉及到两个重要的概念:
Java对象头
Monitor
首先synchronized是一种悲观锁,在操作同步资源前先给同步资源加锁,而加的这把锁是就是存在Java对象头里的,那啥又是Java的对象头?
Java对象保存在内存中时,由3部分组成:
对象头(Header)
实例数据(Instance Data): 对象真正存储的有效信息
对象填充字节(Padding): JVM要求对象的大小必须是8字节的整数倍,对象头已经满足,则当对象的实例数据部分没有对齐时,需要对齐填充来补全。
而对象头又由3部分组成:
Mark Word(标记字段):
Class Metadata Address(类型指针): 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Array Length( 数组长度): 只有数组对象保存了这部分数据。
当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word 记录的是对象和锁有关的信息,默认存储对象的HashCode,分带年龄,锁标志位信息,由于这些信息都是与对象自身定义无关的数据,所以所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
Monitor翻译为监视器,也叫管程,可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
在JVM的规范中,有这么一些话:
“在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的”
“为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁”
“锁住了一个对象,就是获得对象相关联的监视器”
监视器好比一做建筑,它有一个很特别的房间,房间里有一些数据,而且在同一时间只能被一个线程占据,进入这个建筑叫做"进入监视器",进入建筑中的那个特别的房间叫做"获得监视器",占据房间叫做"持有监视器",离开房间叫做"释放监视器",离开建筑叫做"退出监视器".
而一个锁就像一种任何时候只允许一个线程拥有的特权.
一个线程可以允许多次对同一对象上锁.对于每一个对象来说,java虚拟机维护一个计数器,记录对象被加了多少次锁,没被锁的对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器就减1.当计数器跳到0的时候,锁就被完全释放了.
Java虚拟机中的一个线程在它到达监视区域开始处的时候请求一个锁.JAVA程序中每一个监视区域都和一个对象引用相关联.
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
现在再回来看synchronized,synchronized通过Monitor来实现线程同步,而Monitor依赖于底层操作系统的Mutex Lock(互斥锁)实现线程同步
在我之前对自旋锁的描述中,说过:“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。
这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,现在再来看4种锁状态的思路以及特点:
无锁就是没有真正意义上的上锁,所有的线程还是能访问并修改同一个资源,但是通过算法控制,实现同时只有一个线程修改成功。
比如:
CAS全称 Compare and Swap(比较与交换),是一种无锁算法。
在不使用锁的情况下(没有线程被阻塞),实现多线程的变量同步。
CAS算法主要涉及到3个操作数:
①需要进行读写操作的值 V
②判断是否更新的比较值 A
③需要替换值V写入新的值 B
在最开始将V赋值给A,再进行数据操作生成B,需要更新V之前,
比较一下A是否还与V相等,相等说明数据未变化,执行更新操作,
不相等,则更新不能完成。
无锁的特点就是修改操作其实一直在一个循环,线程不断循环尝试修改资源,没有冲突就修改成功,否则继续不断循环尝试。有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。
上面举例的的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁是指一段同步代码一直只被一个线程所访问,那么该线程会自动获取锁,降低获取锁的成本。
例如:家里只有一个碗,但也只有我一个人需要一个碗吃饭,所以不存在争抢,这就是偏向锁
因为在大多数情况下,锁总都是被同一个线程多次反复获得,不存在多线程竞争,所以就出现了偏向锁。目标就是在只有一个线程执行同步代码块时能够提高性能。
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。
我家还是只有一个碗,当我朋友来我家和我一起吃饭,这时候就有两个人,只有这一个碗吃饭,偏向锁升级
偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
我朋友比较了解我吃饭很快,决定等我吃完他再吃,这就是轻量级锁
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
当我和女朋友都很饿的时候,我女朋友觉得我吃的太慢了,不想等我吃完,这时候就会去争抢这唯一的一个碗先吃饭,这是重量级锁状态
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
整体的锁状态升级流程是:无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁
锁状态的改变是根据竞争激烈程度进行的,在几乎无竞争的条件下,会使用偏向锁,在轻度竞争的条件下,会由偏向锁升级为轻量级锁, 在重度竞争的情况下,会升级到重量级锁。
偏向锁通过对比Mark Word 解决加锁问题,避免执行CAS操作。
而轻量级锁是通过CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒带来的性能影响。
重量级锁是将除拥有锁的线程之外的线程全部阻塞。