JVM规范关于Monitor是这么说的。

“在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的”
“为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁”
“锁住了一个对象,就是获得对象相关联的监视器”

内部原理

Synchronized在古老的年代被成为重量级锁。但是java1.6对其进行了优化。为了减少获得锁和锁的释放带来的开销,java1.6为synchronized关键字实现了偏向锁,轻量级锁和重量级锁几种状态

基础知识

首先,必须了解JVM中的对象头。对象头包含一个指向类型元数据的指针(klass point),和运行时数据(Mark Word)。Mark Word包含哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。

java中每个对象都有唯一的一个monitor对应。Mark Word中的LockWord指向自己monitor的起始地址。每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。

下面在网上有人介绍的。没有找到hotspot的源代码支持。但是理论上确实可以实现。
monitor的数据结构如下:

  • Owner 拥有该monitor的线程的唯一标志
  • EntryQ 一个互斥锁(semaphore),是一个计数信号量。用于阻塞线程。由操作系统提供。
  • RcThis 所有被该monitor阻塞的线程个数
  • Nest 可重入锁的计数(持有该monitor的线程可以再次获取此monitor,所以称为可重入,这时候需要一个计数器来表示是否该线程完全释放该锁)
  • HashCode 对应对象的HashCode
  • Candidate 标记是否需要唤醒下一个线程

JVM用monitorenter和monitorexit指令对同步提供显式支持。(sychronized“方法”通常不是用monitorenter和monitorexit指令实现的。往往是由“方法调用指令”检查常数池里的ACC_SYCHRONIZED标志)。JVM会把要加锁的对象放在栈顶,然后执行monitorenter指令,检查那个对象的计数器是否为0或者那个对象是否被当前线程加锁。上述两个条件只要有一个成立,JVM就会对计数器加1并确保该对象的monitor的中的Owner是自己。

synchronized中重量级锁的实现

synchronized依赖于monitorenter的指令。
甲骨文的官方解释如下

Each object has a monitor associated with it. The thread that executes monitorenter gains ownership of the monitor associated with objectref. If another thread already owns the monitor associated with objectref, the current thread waits until the object is unlocked, then tries again to gain ownership. If the current thread already owns the monitor associated with objectref, it increments a counter in the monitor indicating the number of times this thread has entered the monitor. If the monitor associated with objectref is not owned by any thread, the current thread becomes the owner of the monitor, setting the entry count of this monitor to 1.

java 中 Monitor对象是存储在哪里的 java monitor机制_加锁


对应的代码为

java 中 Monitor对象是存储在哪里的 java monitor机制_java_02


简单解释下,aload将局部变量表0位置加载到操作数栈(a表示这是一个引用类型),dup表示复制栈顶元素压栈,astore表示对操作数栈进行POP,将操作数栈的值存储到数组元素1位置。执行完者一切操作,栈顶的元素就是this,此时用monitorenter来请求获取该对象对应的monitor对象。

如果对JVM的栈帧不熟悉,可有参考

这种实现方式可以在JVM中查询到源代码。

java 中 Monitor对象是存储在哪里的 java monitor机制_JVM_03

使用队列维护线程列表,采用阻塞和唤醒机制。

java 中 Monitor对象是存储在哪里的 java monitor机制_加锁_04


ObjectMonitor对象中有两个队列:_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表;

_WaitSet:主要存放所有wait的线程的对象,也就是说如果有线程处于wait状态,将被挂入这个队列。

_EntryList:所有在等待获取锁的线程的对象,也就是说如果有线程处于等待获取锁的状态的时候,将被挂入这个队列。

_owner指向获得ObjectMonitor对象的线程。

java 中 Monitor对象是存储在哪里的 java monitor机制_加锁_05

synchronized的用法

指定加锁对象:对给定对象加锁,进入同步代码前需要活的给定对象的锁。
直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类(当前类内部的class对象)的锁。
synchronized是可重入的。

  • wait() 与 notify/notifyAll 方法必须在同步代码块中使用。否则会有如下异常
Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.notifyAll(Native Method)
	at gdl.ThreadInterruptTest.main(ThreadInterruptTest.java:23)
  • wait() 与 notify/notifyAll() 的执行过程
    由于 wait() 与 notify/notifyAll() 是放在同步代码块中的,因此线程在执行它们时,肯定是进入了临界区中的,即该线程肯定是获得了锁的。
    当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。当执行notify/notifyAll方法时,会唤醒一个处于等待该 对象锁 的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。从这里可以看出,notify/notifyAll()执行后,并不立即释放锁,而是要等到执行完临界区中代码后,再释放。故,在实际编程中,我们应该尽量在线程**调用notify/notifyAll()**后,立即退出临界区。即不要在notify/notifyAll()后面再写一些耗时的代码。
public class ThreadInterruptTest {
    static long i = 0;
    static Object object = new Object();

    public static void main(String[] args) {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (object){
                        object.wait();//该线程释放锁
                        System.out.println("t finish");
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        try {
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        synchronized (object){
            object.notifyAll();//如果去掉这句话那么t finish将永远不会被打印
        }
        System.out.println("main finish");
    }
}

再比如

package example;

public class TestSync {


    public static void main(String[] args) throws Exception{
        Object b = new Object();
        Thread a = new Thread(()->{
            synchronized (b){
                try {
                    b.wait();
                    System.out.println("a finish");
                }catch (Exception ex){

                }
            }
        });

        Thread c = new Thread(()->{
            synchronized (b){
                try {
                    b.notify();
                    Thread.sleep(1000);
                    System.out.println("c finish");
                }catch (Exception ex){

                }
            }
        });

        a.start();
        Thread.sleep(1000);
        c.start();
    }
}

实际上打印的结果是

c finish
a finish

synchronized的其他优化

偏向锁

实现原理为,采用cas替换对象的加锁标志位,将对象头markword中的拥有偏向锁线程id指向自己。然后就可以开心的取执行同步块的内容了。
偏向锁的特点是只要没有人竞争,则该线程一直持有该锁。如果线程2也要竞争该锁,则需要等到没有字节码正在执行的全局安全点。

偏向锁只要发生两个线程的竞争(因为一个线程一旦持有锁就不会释放所以其实就是两个线程申请锁),就会升级为轻量级锁。偏向锁其实是JVM对那些根本没必要加锁的代码的优化。

轻量级锁

轻量级锁使用了自旋锁,线程会首先尝试自旋获取锁,但是自旋次数会进行限制,超过这个限制后会转而使用阻塞锁,此时轻量级锁就会膨胀为阻塞锁。

几种状态的优缺点

偏向锁
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。
如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

轻量级锁
竞争的线程不会阻塞,提高了程序的响应速度。
如果始终得不到锁竞争的线程使用自旋会消耗CPU。

重量级锁
线程竞争不使用自旋,不会消耗CPU。
线程阻塞,响应时间缓慢。
这是因为线程的阻塞和唤醒都是cpu核心态的代码。执行阻塞和唤醒都需要cpu进行状态切换,频繁的切换状态对cpu负担很重。

锁的升级过程

java 中 Monitor对象是存储在哪里的 java monitor机制_java_06