浅析 Kotlin 中的 synchronized

作者: 光剑书架上的书 | 来源:发表于2021-03-17 20:03 被阅读0次

    首先,在 Java 中 synchronized 是一个关键字,在Kotlin 中是一个函数。这个函数如下:

    /*
     * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
     * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
     */
    
    @file:kotlin.jvm.JvmMultifileClass
    @file:kotlin.jvm.JvmName("StandardKt")
    package kotlin
    
    import kotlin.contracts.*
    import kotlin.jvm.internal.unsafe.*
    
    /**
     * Executes the given function [block] while holding the monitor of the given object [lock].
     */
    @kotlin.internal.InlineOnly
    public actual inline fun <R> synchronized(lock: Any, block: () -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
    
        @Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE", "INVISIBLE_MEMBER")
        monitorEnter(lock)
        try {
            return block()
        }
        finally {
            @Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE", "INVISIBLE_MEMBER")
            monitorExit(lock)
        }
    }
    
    

    Decompile成字节码:

    可以看出:这里边也是有monitorenter和monitorexit的,所以做出推测,不管synchronized是java中的关键字还是kotlin中的函数,最终被编译成的字节码是一样的。

    关于:contract{ ... } Kotlin 的契约编程, 参考:https://blog.csdn.net/universsky2015/article/details/99011895

    Java synchronized 实现原理

    在《深入理解Java虚拟机》一书中,介绍了HotSpot虚拟机中,对象的内存布局分为三个区域:对象头(Header)、实例数据(Instance Data)和对齐数据(Padding)。而对象头又分为两个部分“Mark Word”和类型指针,其中“Mark Word”包含了线程持有的锁。
      因此,synchronized锁,也是保存在对象头中。JVM基于进入和退出Monitor对象来实现synchronized方法和代码块的同步,对于方法和代码块的实现细节又有不同:

    代码块,使用monitorenter和monitorexit指令来实现;monitorenter指令编译后,插入到同步代码块开始的位置,monitorexit指令插入到方法同步代码块结束位置和异常处,JVM保证每个monitorenter必须有一个monitorexit指令与之对应。线程执行到monitorenter指令处时,会尝试获取对象对应的Monitor对象的所有权 (任何一个对象都有一个Monitor对象预制对应,当一个Monitor被持有后,它将处于锁定状态) 。

    方法:在《深入理解Java虚拟机》同步指令一节中,关于方法级的同步描述如下:

    方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程获取了管程,其他线程就无法获取管程。

    synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

    Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

    • 普通同步方法,锁是当前实例对象
    • 静态同步方法,锁是当前类的class对象
    • 同步方法块,锁是括号里面的对象

    HotSpot虚拟机中,对象的内存布局分为三个区域:

    • 对象头(Header
    • 实例数据(Instance Data
    • 对齐填充(Padding

    其中,对象头(Header)又分为两部分:

    • Mark Word
    • 类型指针

    synchronized用的锁是存储在Java对象头的Mark Word中的。

    下面是Mark Word的存储结构(32位JVM):

    锁状态 25bit 4bit 1bit,是否是偏向锁 2bit,锁标志位
    无锁状态 对象的hashCode 对象分代年龄 0 01

    在运行期,Mark Word里存储的数据会随着标志位的变化而变化。

    存储内容 标志位 状态
    指向栈中锁记录的指针 00 轻量级锁
    指向互斥量(重量级锁)的指针 10 重量级锁
    空,不需要记录信息 11 GC标记
    偏向线程ID、偏向时间戳、对象分代年龄 01 偏向锁

    可以看到,Mark Word包含了线程持有的锁。

    JVM基于进入和退出Monitor对象来实现sunchronized方法和代码块的同步,两者细节上有差异。

    1.1 synchronized代码块

    使用monitorentermonitorexit指令来实现。

    minitorenter指令编译后,插入到同步代码块开始的位置,monitorexit指令编译后,插入到同步代码块结束的位置和异常处。JVM保证每个monitorenter必须有一个monitorexit指令与之对应。

    每个对象都有一个Monitor对象(监视器锁)与之对应。

    • monitorenter

    当线程执行到monitorenter指令的时候,将会尝试获取Monitor对象的所有权,过程如下:

    1. 如果Monitor对象的进入计数器为0,则该线程成功获取Monitor对象的所有权,然后将计数器设置为1
    2. 如果该线程已经拥有了Monitor的所有权,那这次算作是重入,重入也会将计数器的值加1
    3. 如果其他线程已经占有了Monitor对象,那么该线程进入阻塞状态,直到Monitor的计数器的值为0,再重新尝试获取Monitor对象的所有权。
    • monitorexit

    当已经获取Monitor对象所有权的线程执行到monitorexit指令的时候,将会释放Monitor对象的所有权。过程如下:

    1. 执行monitorexit指令时,Monitor对象的进入计数器的值减1,如果减1后的值为0,那么这个线程将会释放Monitor对象的所有权,其他被这个Monitor阻塞的线程可以开始尝试去获取这个Monitor对象的所有权。
    public class com.fufu.concurrent.SyncCodeBlock {
      public int i;
    
      public com.fufu.concurrent.SyncCodeBlock();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public void syncTask();
        Code:
           0: aload_0
           1: dup
           2: astore_1
           3: monitorenter                      //注意此处,进入同步方法
           4: aload_0
           5: dup
           6: getfield      #2                  // Field i:I
           9: iconst_1
          10: iadd
          11: putfield      #2                  // Field i:I
          14: aload_1
          15: monitorexit                       //注意此处,退出同步方法
          16: goto          24
          19: astore_2
          20: aload_1
          21: monitorexit                      //注意此处,退出同步方法
          22: aload_2
          23: athrow
          24: return
        Exception table:
           from    to  target type
               4    16    19   any
              19    22    19   any
    }
    
    

    1.2 synchronized方法

    方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。

    JVM可以从 方法常量池 中的 方法表结构(method_info Structure 中的 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法。

    当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程获取了管程,其他线程就无法获取管程。

      //省略没必要的字节码
      //==================syncTask方法======================
      public synchronized void syncTask();
        descriptor: ()V
        //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: dup
             2: getfield      #2                  // Field i:I
             5: iconst_1
             6: iadd
             7: putfield      #2                  // Field i:I
            10: return
          LineNumberTable:
            line 12: 0
            line 13: 10
    
    

    2 synchronized使用规则

    参考:https://www.cnblogs.com/skywang12345/p/3479202.html

    下面总结了对象的synchronized基本规则。

    • 规则一:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的这个 “synchronized方法” 或者这个 “synchronized代码块” 的访问将被阻塞。

    • 规则二:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的其他的 “synchronized方法” 或者其他的 “synchronized代码块” 的访问将被阻塞。

    • 规则三:当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程仍然可以访问 “该对象” 的非同步代码块

    2.1 规则一

    当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的这个 “synchronized方法” 或者这个 “synchronized代码块” 的访问将被阻塞。

    public class Demo1 {
    
        public static void main(String[] args) {
    
            UserRunnable r = new UserRunnable();
            Thread t1 = new Thread(r, "thread-1");
            Thread t2 = new Thread(r, "thread-2");
            t1.start();
            t2.start();
        }
    }
    
    class UserRunnable implements Runnable {
    
        @Override
        public void run() {
            synchronized (this) {
                try {
                    for (int i = 1; i <= 3; i++) {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + " loop " + i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

    运行结果:

    thread-1 loop 1
    thread-1 loop 2
    thread-1 loop 3
    thread-2 loop 1
    thread-2 loop 2
    thread-2 loop 3
    
    Process finished with exit code 0
    
    

    可以看到,线程thread-1获得了r对象的锁,执行同步代码块,线程thread-2只能等待线程thread-1执行完了才能开始执行。

    2.2 规则二

    当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程对“该对象” 的其他的 “synchronized方法” 或者其他的 “synchronized代码块” 的访问将被阻塞。

    public class Demo2 {
    
        public static void main(String[] args) {
    
            Obj obj = new Obj();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    obj.methadA();
                }
            }, "thread-1");
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    obj.methadB();
                }
            }, "thread-2");
            t1.start();
            t2.start();
        }
    }
    
    class Obj {
    
        public void methadA() {
            synchronized (this) {
                try {
                    for (int i = 1; i <= 3; i++) {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() 
                                           + " call methodA, loop " + i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public void methadB() {
            synchronized (this) {
                try {
                    for (int i = 1; i <= 3; i++) {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() 
                                           + " call methodB, loop " + i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

    运行结果:

    thread-1 call methodA, loop 1
    thread-1 call methodA, loop 2
    thread-1 call methodA, loop 3
    thread-2 call methodB, loop 1
    thread-2 call methodB, loop 2
    thread-2 call methodB, loop 3
    
    Process finished with exit code 0
    
    

    可以看到,Obj类中的methodAmethodB方法都有一个同步代码块。当线程thread-1调用obj对象的methodA方法的时候,线程thread-2被阻塞了,直到thread-1释放了obj对象的锁,thread-2才开始调用methodB方法。

    2.3 规则三

    当一个线程访问 “某对象” 的 “synchronized方法” 或者 “synchronized代码块” 时,其他线程仍然可以访问 “该对象” 的非同步代码块

    public class Demo3 {
    
        public static void main(String[] args) {
    
            Obj obj = new Obj();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    obj.methadA();
                }
            }, "thread-1");
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    obj.methadB();
                }
            }, "thread-2");
            t1.start();
            t2.start();
        }
    }
    
    class Obj {
    
        public void methadA() {
            synchronized (this) {
                try {
                    for (int i = 1; i <= 3; i++) {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() 
                                           + " call methodA, loop " + i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public void methadB() {
            try {
                for (int i = 1; i <= 3; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() 
                                       + " call methodB, loop " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    运行结果:

    thread-1 call methodA, loop 1
    thread-2 call methodB, loop 1
    thread-1 call methodA, loop 2
    thread-2 call methodB, loop 2
    thread-1 call methodA, loop 3
    thread-2 call methodB, loop 3
    
    Process finished with exit code 0
    
    

    可以看到,Obj类的methodA方法有同步代码块,而methodB方法没有。当线程thread-1访问methodA方法的时候,线程thread-2可以访问methodB方法,不会阻塞。

    3 实例锁 和 全局锁

    实例锁

    • 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
    • 实例锁对应的就是 synchronized关键字。

    全局锁

    • 该锁针对的是类,无论实例多少个对象,线程都共享该锁。
    • 全局锁对应的就是 static synchronized关键字(或者是锁在该类的class或者lassloader对象上)。

    例子:

    pulbic class Something {
        public synchronized void syncA(){}
        public synchronized void syncB(){}
        public static synchronized void cSyncA(){}
        public static synchronized void cSyncB(){}
    }
    
    

    假设Something有两个实例xy,结论:

    1. x.syncA()x.syncB()不能被同时访问。因为使用了同一个对象的实例锁。
    2. x.syncA()y.syncB()可以被同时访问。因为使用了不同实例对象的实例锁。
    3. x.cSyncA()y.cSyncB()不能被同时访问。因为他们使用了同一个全局锁,相当于Something类的锁。
    4. x.syncA()Something.cSyncA()可以被同时访问。因为一个是实例x的锁,一个是类Something的锁,不是同一个锁,互不干扰。

    参考资料

    https://juejin.cn/post/6844903830644064264
    https://blog.csdn.net/hbtj_1216/article/details/77773292
    《深入理解Java虚拟机》
    《Java并发编程艺术》
    【死磕Java并发】—–深入分析synchronized的实现原理
    JVM源码分析之synchronized实现

    相关文章

      网友评论

        本文标题:浅析 Kotlin 中的 synchronized

        本文链接:https://www.haomeiwen.com/subject/zbmecltx.html