Synchronized简介
Java中提供了两种实现同步的基础语义: synchronized 方法和 synchronized 块 ,先来个案例进行分析!
public class SyncTest {
public void syncBlock(){
synchronized (this){
System.out.println("sync block balabala....");
}
}
public synchronized void syncMethod(){
System.out.println("sync method hahaha....");
}
public static void main(String[] args) {
}
}
将SyncTest.java 编译为SyncTest.class文件,我们使用 javap -v SyncTest.class 查看class文件对应的JVM字节码信息。这里我使用的是JVM版本是JDK1.8。首先看看syncBlock()方法的字节码:
再看看syncMethod()方法的字节码:
从上面字节码可以看出,对于Synchronized 关键字而言,javac 在编译时,会生成对应的 monitorenter 和monitorexit指令,分别对应sync同步块进入和退出同步代码块,这里读者很容易发现有两个monitorexit 退出指令,原因是为了保证在程序抛出异常时最终也会释放锁,
所有javac为同步代码块添加了一个隐式的try-finally,在finally中会调用 monitorexit 指令释放锁。而对于Synchronized方法而言,javac 为其生成一个 ACC_SYNCHRONIZED 关键字,在JVM进行方法调用时,发现调用的方法被 ACC_SYNCHRONIZED修饰时,则会先尝试获取锁。
锁的几种形态
依赖于系统的同步函数,这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高,这使得传统意义上的锁(重量级锁)效率低下。
在JDK1.6 之前,Synchronized 只有传统意义上的锁,而在JDK1.6进口了两种新型锁机制(偏向锁和轻量级锁),它们的引入是为了解决在多线程并发不高场景下使用传统锁(重量级锁)带来的性能开销问题。
在了解这几种锁的实现机制之前,我们先来了解下对象头,它是多种锁机制的基础。
对象头
因为在java中任意对象都可以用作锁,因此必然需要有一个映射关系(存储该对象及其对应的锁信息),比如当前那个线程持有锁,哪些线程在等待。这就有点类似于我们学习的Map,但是如果使用Map来记录这些对应关系,需要保证Map集合的线程安全问题,
不同的Synchronized之间会相互影响,性能差,另外,当同步对象比较多时,该Map会占用比较多的内存。
why 使用对象头?因为对象头本身也有一些hashcode、GC相关数据。在JVM中,对象在内存中除了本身数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark work 和类型指针。
另外对于数组而言还会有一份记录数组长度的数据。mark work用于存储对象的hashcode 、GC分代年龄、锁状态等信息。在32位系统上 mark work长度是32bit,64位系统是64bit.为了能在有限的空间中存储更多的信息,其存储格式是不固定的,下面分别对应32bit 操作系统和64bit操作系统:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |状态
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |无锁态
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |偏向锁
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |轻量级锁
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |重量级锁
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |GC标记
|-------------------------------------------------------|--------------------|
|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |状态
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |无锁态
|------------------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |偏向锁
|------------------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | Lightweight Locked |轻量级锁
|------------------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked |重量级锁
|------------------------------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |GC标记
|------------------------------------------------------------------------------|--------------------|
可以看到锁信息是存在对象的 mark work 中的。当对象状态为:
重量级锁
当状态为重量级锁(Heavyweight Locked)
时存储的是指向堆中的monitor
对象的指针 。那么这个monitor
对象包括哪些信息呢?
一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是ObjectWaiter的链表结构,owner指向持有锁的线程。
JVM 每次从队列尾部取出一个数据用于锁竞争候选者(OnDeck),但是在并发情况下,ContentionList 会被大量的并发线程进行CAS访问,为了降低尾部元素的竞争,JVM会将一部分线程移动到 EntryList 中作为候选竞争线程。Owner线程并不是直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,这样虽然牺牲了一些公平性,但是能极大提高系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList 中,如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻被notify或者notifyAll唤醒,会重新进入EntryList 中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的。
Synchronized是非公平锁,在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
偏向锁
偏向锁,顾名思义,它会偏向第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程竞争的情况,则线程是不需要触发同步机制,在这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其它线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁!
加锁过程
解锁过程
偏向锁的撤销在上述第四步中有提到。偏向锁只有遇到其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤离需要等待全局安全点(在某个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为01)或轻量级锁(标志位为00)的状态。
另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0
来关闭延迟。
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入到锁争用的时候,偏向锁就会升级为你轻量级锁;
加锁过程
解锁过程