synchronized介绍

synchronized相信大家都比较熟悉,面试的时候也是必备问题。有很多书籍介绍它,网上也有很多很多相关的文字,我自己也看过很多。但是看了就忘,印象不深,于是我决定阅读源码来加深记忆。代码出真知,这一看不要紧,不仅让我对synchronized有了更深入的理解,我还发现,网上许多文章,甚至很多面试官的理解都是错误的。

比如,说说你对自旋锁的理解。你可以先把答案自己在心里想一下,看到下面,你可能会发现,原来你原来的理解是错误的。

synchronized使用

使用上我就不多说了,相信大家都知道,这里概括一下

synchronized(obj) {
   ....
   some code
   ...
}

这种使用时对obj对象上锁。也是比较常见的使用。

synchronized void test() {
    ....
    some code
    ...
}

这种事对this进行上锁

synchronized static void test() {
    ....
    some code
    ...
}

这种事对Class对象上锁。

synchronized字节码表示

加入我们有一个简单的方法如下

public void test() {
    Object obj = new Object();
    synchronized (obj) {
        System.out.println("in locking");
    }
}

会编译成以下字节码

 public test()V
   ...
    MONITORENTER
   L0
    LINENUMBER 15 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "in locking"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L6
    LINENUMBER 16 L6
    ALOAD 2
    MONITOREXIT
   L1
    GOTO L7
   L2
   FRAME FULL [com/pinduoduo/service/jmm/JmmTest java/lang/Object java/lang/Object] [java/lang/Throwable]
    ASTORE 3
    ALOAD 2
    MONITOREXIT
    ...
}

其他我们不要管,可以看到字节码中有一个MONITOREXIT指令和两个MONITOREXIT指令。两个MONITOREXIT是因为要异常的情况也也要进行MONITOREXIT.

synchronized的jvm表示

我们在上小节知道了synchronized块是有MONITORENTERMONITOREXIT指令包裹着的,那这两个指令怎么执行呢?这就涉及的jvm怎么实现的了,这里我们就以hotspot为例,看看他是怎么实现的。

hotspot的这两个指令执行入口在bytecodeInterpreter.cpp中的CASE(_monitorenter)CASE(_monitorexit)中,接着就是一堆c++代码了,这里我们不深入,不然大家肯定睡着了,有兴趣的小伙伴可以自己去看看,我其他文章也会具体分析。

synchronized原理

这一小节会简单的说一说synchronized的原理,都是根据我阅读源码的总结。

偏向锁

偏向锁适用于没有多个线程会进入的资源。这个点在说轻量级锁的时候会再次提到。

偏向锁又称无锁,因为它只改变锁对象头的markWord。

在Hotspot中,每个java对象并不是对应一个c++对象,而是用oop-klass这种二分模型表示,关于这个二分模型我们也不展开说。oop中一个一个markOop,他的类型是 markOopDesc*,是一个指针。而markword就是指针的值,而不是指针指向的地址的值。啥意思呢?比如64位机器上,markOop就是一个64位的数值而已,这一点也是我饶了很久才懂。这里只是提一下,不重要。

结构图如下:
image

偏向锁状态下,markword的锁标志位会变成01,是否偏向标志为1,此外epoch只一个标志,用于是否需要批量重定向的判断,我们先不管它。那如何证明偏向锁状态下,锁的markword会变成这个样子呢?我们的jol就登场了,跟我一起看接下来的小demo,耐心。

首先我们引入jol的依赖

 <dependency>
     <groupId>org.openjdk.jol</groupId>
     <artifactId>jol-core</artifactId>
     <version>0.9</version>
</dependency>

代码如下

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

public class MarkWordTest {

    public static void main(String[] args) throws InterruptedException {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        MarkWordTest object = new MarkWordTest();
        String classLayout1 = ClassLayout.parseInstance(object).toPrintable();
        System.out.println("---------------------------加锁前---------------------------");
        System.out.println(classLayout1);
        System.out.println("------------------------------------------------------------");
        synchronized (object) {
            //打印当前jvm信息
            System.out.println("---------------------------加锁中---------------------------");
            String classLayout2 = ClassLayout.parseInstance(object).toPrintable();
            System.out.println(classLayout2);
            System.out.println("------------------------------------------------------------");
        }
        System.out.println("---------------------------加锁后---------------------------");
        String classLayout3 = ClassLayout.parseInstance(object).toPrintable();
        System.out.println(classLayout3);
        System.out.println("------------------------------------------------------------");
    }

}

首先注意到,代码开始有一个sleep 5秒的操作,是因为偏向锁默认有个延迟启动时间,默认为4s。

然后分别在加锁前、中、后对对象头进行打印,我们看一下打印结果


---------------------------加锁前---------------------------
com.pinduoduo.service.jmm.MarkWordTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

------------------------------------------------------------
---------------------------加锁中---------------------------
com.pinduoduo.service.jmm.MarkWordTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

------------------------------------------------------------
---------------------------加锁后---------------------------
com.pinduoduo.service.jmm.MarkWordTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

------------------------------------------------------------

Process finished with exit code 0

markword是64位,既上面一堆数字的前两行,我们删掉其他东西,只看markword

---------------------------加锁前---------------------------
...
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
...
------------------------------------------------------------
---------------------------加锁中---------------------------                     
...
      0     4        (object header)                           05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
...
------------------------------------------------------------
---------------------------加锁后---------------------------
...
      0     4        (object header)                           05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
...
------------------------------------------------------------

是不是有点看不懂,这是因为,Hopspot是小端储存,既低位值放在高位地址上。那我们按照我们好读的宗旨把它改为大端存

---------------------------加锁前---------------------------
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
------------------------------------------------------------
---------------------------加锁中---------------------------                     
00000000 00000000 00000000 00000000 00000011 01100111 00111000 00000101

------------------------------------------------------------
---------------------------加锁后---------------------------
00000000 00000000 00000000 00000000 00000011 01100111 00111000 00000101
------------------------------------------------------------

现在就是我们可以读懂的结构了。

等等,加锁前为啥锁标志位为01,偏向锁模式呢?是的,是这样的,对象头初始就是偏向锁模式,并不是很多文章都说的无锁模式,只是现在还没有指向任何线程。

加锁中我们就好理解了,锁标志位是偏向锁,并且线程id也不是0了。

加锁后可以看到,markword并没有任何改变。这样做自然有用,就是,当有其他线程来想进入临界区的时候,看到偏向线程id不是自己,直接进行锁升级(不考虑批量重偏向)。

至此我们可以得出一个结论:偏向锁只在只有一个线程会进入临界区的场景有用。

不知道你有没有注意到一个问题,就是,既然退出临界区后,markword没有任何变化,那锁重入的计数怎么实现的?这就涉及到synchronized的重入了,不管是偏向锁、轻量锁还是重量级锁,都是使用的同一套重入机制。

自旋锁的重入

在Hopspot中,每个线程都有一个锁记录表,锁记录是BasicObjectLock类型,里边有一个obj和displaced_header,obj存储锁对象,set_displaced_header锁对象的markword,每一次进入临界区,都会创建一个BasicObjectLock对象,如果是重复,那displaced_header就置为null.

image

轻量级锁

轻量级锁适用于线程交替进入临界区的场景,只要有锁竞争就会升级成重量级锁。注意,1.8中<font color=red>没有自旋锁</font>,别再说经过自旋后升级成重量级锁!!!

别嫌我啰嗦,再比较一下偏向锁与轻量级锁适用场景的不同。

偏向锁:只有一个线程会进入临界区

轻量级锁:不同线程交替进入临界区

偏向锁和轻量级锁都是在没有锁竞争的条件下适用的。

轻量级锁的加锁和释放都是通过cas操作对锁对象的markword就行修改而实现的,它的重入与偏向锁的机制相同,这里就不赘述了。

重量级锁

重量级锁的实现是用了监视器模式(moniter),我猜字节码指令MONITORENTERMONITOREXIT开始指的就是这个操作,后来经过一系列的优化,使这两个指令的字面意义跟锁的实现不太对应了。

在Hotspot中,每个对象(java对象)都会关联一个ObjectMonitor,这个就是一个监视器。

ObjectMonitor中我们重点关注以下几个字段

 ObjectMonitor() {
    _owner        = NULL; // 锁持有者
    _recursions   = 0; // 重入次数
    _WaitSet      = NULL;
    _cxq          = NULL ;
    _EntryList    = NULL ;
  }

_owner既为锁持有者。

_recursions表示重入次数。从这个字段就可以看出来,重量级锁的重入跟轻量级锁、偏向锁的重入不同,是用计数法来做的。

_cxq是一个队列,如果线程获取锁失败,一定是进入这个队列的。

_EntryList也是一个人队列,当ObjectMonitor持有者释放锁后,ObjectMonitor一定是从这个队列中唤醒一个线程的。

_WaitSet是调用wait操作的线程队列。

认识这几个字段后,我们会很容易理解ObjectMonitor的运作原来,并没有网上说的那么复杂,至于网上经常晒的那张流程图我就不贴了。下面我概括一下。

  • 线程要进入临界区的时候,如果_owner为空,则直接用cas去修改_owner的值,成功则证明获取锁成功,否则,会准备进去队列,在进入队列前,会经过多次自旋尝试cas修改_owner,这也是为什么synchronized,是非公平锁的原因,就是他会不断的抢占式的获取锁。
  • 多次尝试无果后,线程会进入_cxq队列中。注意,抢占无果的线程一定是进入_cxq队列。
  • 线程在获取锁后,如果调用lock#wait方法,那这个线程会进入_WaitSet队列。
  • 当获取锁的线程调notify或notifyAll时,根据不同的策略,_WaitSet会将节点插入_cxq_EntryList中。

    0: 将头节点插入到EntryList头部
    1: 将头节点插入到EntryList尾部
    2: 将头节点插入到cxq头部,默认选项
    3: 将头节点插入到cxq尾部

当锁空闲的时候,根据不同的策略,以不同的方式将_cxq插入到_EntryList

QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,后面的代码不会执行了;
QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;
QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;
QMode = 0:暂时什么都不做
  • 从队列中取线程来进入临界区:
    如果EntryList的首元素非空,就取出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回;
    如果EntryList的首元素为空,就将cxq的所有元素放入到EntryList中,然后再从EntryList中取出来队首元素执行ExitEpilog方法,然后立即返回;
  • 如果是重入则会_recursions++

总结

  • 偏向锁适用于只有一个线程会进入临界资源的情景,只要有多线程进入临界区就会导致锁升级
  • 轻量级锁适合多线程交替进入临界区的情景,只要有多线程同时想进入临界区就会导致锁升级
  • 偏向锁和轻量级锁的重入都是由线程中的锁记录来实现的
  • 重量级锁采用Moniter实现。根据不同策略和操作,线程在 _WaitSet _cxq _EntryList 中流转,重入通过_recursions计数来实现。

程序员训练生
1 声望1 粉丝