解析Java中synchronized关键字的同步机制原理

181 阅读5分钟

关于synchronized的使用,我在上一篇文章确保多线程环境下数据一致性的关键:synchronized的应用写了。接下来进入原理的部分。

先看下面一段代码:

public class SynchronizeTheory {
    public void method() {
        // 省略代码逻辑...
        System.out.println("normal code.");
    }
}

假设现在这是一段普通的代码逻辑,我们将其编译一下,然后通过javap看一下已编译类文件的详细信息。

可以看到一些信息比如版本号、MD5、常量池、构造器、方法等信息,找到method方法可以看到标志位显示的是ACC_PUBLIC,说明该方法是一个PUBLIC方法。

现在,假设执行method方法这段代码逻辑时存在临界资源,多线程下可能会有数据不一致的情况,我们需要把这段方法改成一个同步方法,加上synchronized关键字,如下

public class SynchronizeTheory {

    public synchronized void method() {

        System.out.println("进入同步代码块");

        // 省略同步逻辑...

        System.out.println("Synchronized code.");

    }

}

再看下编译后的信息:

可以看到,标志位在原有ACC_PUBLIC的基础上,又新增了ACC_SYNCHRONIZED标识符。说明method还是一个同步方法

到这里可以解释synchronized能够实现同步的原理了。在Java字节码中,使用synchronize关键字标记的方法(或代码块)在编译为字节码时会通过 monitorentermonitorexit 指令来实现同步。monitorenter对应同步代码块的开始位置,monitorexit指令对应同步代码块的结束位置。从行号表中也可以看出对应关系:

这张图再结合一下源代码就清晰了:

在源代码第13行时进入到同步代码块中,在第19行时退出同步代码块。结合monitorenter和monitorexit指令的执行时机体现在程序里就是:

在进入同步方法时也就是第13行,执行monitorenter指令,这时会尝试获取相关对象的Monitor锁,如果锁未被其他线程占用,则该线程将获得锁并进入Monitor。如果锁已被其他线程持有,线程将被阻塞直到锁被释放。当执行到代码19行时,这时要退出同步方法,会执行monitorexit指令,会释放持有的Monitor锁

这时你可能会有很多疑问,比如从字节码中没有看到这两条指令啊?或者Monitor锁又是啥...

不急,这几个问题我会一个个说来,说这些之前先解释下锁的可重入性。synchronized是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入锁

结合可重入性再补充一下上面说的这两条指令的执行机制:

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。进入同步代码块的线程会通过monitor的进入数来判断是否可进入,如果monitor的进入数为0,该线程进入,也就是monitorenter指令执行成功,这是进入数就设置为1,并且该线程为monitor的持有者。

如果该线程已经占有了该monitor(每个对象头中会有偏向线程ID信息,以此来定位所属线程),只是重新进入,则monitor的进入数加1。如果是其他线程占用monitor,则线程进入阻塞状态,等待monitor的进入数为0,再重新尝试进入。

所以在执行monitorexit时,进入数也要减1。如果某个线程重入了monitor,完全退出时也要执行相应的释放次数。确保monitor进入数为0。当monitor的进入数为0时,这个线程也就是释放了持有权。

再说下为何字节码中没有这两条指令的信息?

这里其实只是没有直接的体现,由于反编译器对字节码进行了优化或简化,将synchronized方法的同步实现以更简洁的方式表示出来了,在实际执行时,Java虚拟机会根据synchronize的语义来确保同步。monitorenter指令用于获取对象的监视器锁,而monitorexit指令用于释放锁。这些指令会在同步块的开始和结束位置隐式地执行。这也是因为同步的实现是由Java虚拟机负责的,而不是由反编译器生成的字节码所体现

那Monitor监视器锁又是什么?

在Java虚拟机中,每个对象都会与一个Monitor关联。它通常被描述为一个对象。与一切皆对象一样,所有的Java对象都有成为Monitor的潜质。因为在Java的设计中 ,每一个Java对象设计中就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在HotSpot实现的Java虚拟机中,Monitor是由ObjectMonitor实现的:

我们用synchronized实现了同步方法,Java代码编译成字节码后通过标识符ACC_SYNCHRONIZED来判断同步状态,通过monitor指令获取Monitor监视器锁,monitorexit释放监视器锁,由Hotspot中的ObjectMonitor来实现获取与释放的逻辑

这样子串起来以后,是不是容易理解一些了,通过加深对synchronized的理解,来帮助我们写出高效且线程安全的Java程序。