synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重 入锁。
1. 作用于方法时,锁住的是对象的实例(this);
2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁, 会锁所有调用该方法的线程;
3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列, 当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
synchronized有三种方式来加锁,分别是:方法锁,对象锁synchronized(this),类锁synchronized(Demo.Class)。
public class SynchronizedTest1 {
private static int count = 0;
public static void test(){
synchronized (SynchronizedTest1.class) {
count++;
}
}
}
通过javap -v 来查看对应代码的字节码指令:
对于同步块的实现使用了monitorenter和monitorexit指令:他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。
public class SynchronizedTest2 {
private static int count = 0;
public synchronized static void test() {
count++;
}
}
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。
JVM通过该ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。
Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
JVM中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度)。
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
以 32位虚拟机为例:
无锁:对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放对象分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01
偏向锁: 在偏向锁中划分更细,还是开辟 25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 Epoch,4bit 存放对象分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01
轻量级锁:在轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00
重量级锁: 在重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11
GC标记: 开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。
关于内存的分配,在git中openJDK中 markOop.hpp:
管程首先由霍尔(C.A.R.Hoare)和汉森(P.B.Hansen)两位大佬提出,是一种并发控制机制,由编程语言来具体实现。它负责管理共享资源以及对共享资源的操作,并提供多线程环境下的互斥和同步,以支持安全的并发访问。
管程能够保证同一时刻最多只有一个线程访问与操作共享资源(即进入临界区)。在临界区被占用时,其他试图进入临界区的线程都将等待。如果线程不能满足执行临界区逻辑的条件(比如资源不足),就会阻塞。阻塞的线程可以在满足条件时被唤醒,再次试图执行临界区逻辑。
“共享资源以及对共享资源的操作”在操作系统理论中称为critical section,即临界区。
所有的Java对象是天生的Monitor。synchronized借助Java对象中的Monitor完成线程的阻塞。加锁就是在竞争 monitor 对象。
Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。
只有重量级锁才会借助Monitor,而偏向锁和轻量级锁均借助的是对象头中
mark word
的标识来实现的。
管程的设计有Hansen、Hoare和Mesa三种模型。Mesa是Java采用的设计方案。
图中有两个条件变量a、b,它们对应的线程队列为a.q和b.q,另外还有一个入口队列e,它们分别占用一个房间。右下角的大房间即为临界区。该模型的执行流程如下:
Mesa管程的特点是:线程由阻塞状态被唤醒之后不会立即执行,而是回到入口等待。相对地,Hoare管程在线程被唤醒后就会立即切换上下文,让被唤醒的线程先执行。后者的实现简单,但会触发更多的上下文切换操作,浪费CPU时间。前者的效率自然比较高,但带来的潜在问题是线程回到队列e后,原先满足的条件可能已经不再满足,必须重新检查。所以在Mesa管程模型下编写程序时,检查条件应该用while,而不是if:
while (!condition) {
wait(a)
}
在 Java 程序运行的过程中,每创建一个新的对象,在 JVM 内部就会相应地创建一个对应类型的 opp(普通对象指针) 对象。各种 oop 类的共同基类为 oopDesc
类。所以每个object对象都包含markOop。
在hotspot虚拟机中,采用ObjectMonitor类来实现monitor。
每个object的对象里markOop->monitor() 里可以保存ObjectMonitor的对象。所以这也是为什么每个对象都能成为锁的原因之一。
oop.hpp
markOop.hpp
class oopDesc {//顶层基类
friend class VMStructs;
private:
volatile markOop _mark;//每个对象的mark头
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
}
openJDK objectMonitor.hpp
_count:线程获取管程锁的次数;
_recursions:管程锁的重入次数;
_owner:指向持有ObjectMonitor对象的线程
_waiters:处于等待状态的线程数;
_WaitSet:处于等待状态的线程队列(双向链表);
_EntryList:管程的入口线程队列(双向链表);
_cxq:线程竞争管程锁时的队列(单向链表);
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter)
其中,_EntryList就相当于Mesa管程模型中的队列e,而_WaitSet就相当于其中的队列a.q或者b.q。Object.wait()/notify()/notifyAll()三个方法也会直接映射到ObjectMonitor的同名方法。由此也可见,ObjectMonitor只有一个隐式的条件变量,及与其相关的线程队列。_EntryList、_WaitSet和_owner之间的关系如下图所示:
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会通知等待线程可以醒了,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。
wait()/notify()/notifyAll()为什么存在于Object顶级对象中?
Object类中的wait()/notify()/notifyAll()都是native方法,其真正实现是C++中的objectMonitor.cpp
。它们都是monitor的函数,而每个java对象都与一个monitor关联,所以wait()/notify()/notifyAll()存在于Object对象中。
wait和notify为什么需要在synchronized里面?
wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。
synchronized原理
管程
synchronized底层实现monitor详解
说一说管程(Monitor)及其在Java synchronized机制中的体现