美文网首页
并发编程专题 3:synchronized

并发编程专题 3:synchronized

作者: 开发者如是说 | 来源:发表于2019-04-08 21:28 被阅读0次

    1、synchronized 修饰的几种情形

    根据 synchronized 可以被用来修饰的对象可以分成下面几种情形,

    [图片上传失败...(image-2db02-1554730114966)]

    2、synchronized 的原理

    2.1 理论基础

    Java 虚拟机中的同步 (Synchronization) 基于进入和退出管程 (Monitor) 对象实现,无论是显式同步 (有明确的 monitorentermonitorexit 指令,即同步代码块),还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的

    在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。其中,对象头是实现synchronized 的基础。synchronized 使用的锁对象是存储在 Java 对象头里的,JVM 中采用 2 个字来存储对象头 (如果对象是数组则会分配 3 个字,多出来的 1 个字记录的是数组长度),其主要由 Mark Word 和 Class Metadata Address 组成。

    对象头

    在默认情况下 Mark Word 的数据结构,

    默认情况下 Mark Word 的数据结构

    Mark Word 是一个非固定的数据结构,会根据对象自身的状态复用自己的存储空间。如 32 位 JVM 下,还有如下可能变化的结构:

    Mark Work 可能的结构

    重量级锁也就是 synchronized 的对象锁,锁标识位为 10,其中指针指向的是 monitor 对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在 Java 虚拟机 (HotSpot) 中,monitor 由 ObjectMonitor 实现,其主要数据结构如下,

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0;     // 记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;
        _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }
    

    ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程同时 monitor 中的计数器 count 加 1,若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为null,count 自减 1,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor (锁)并复位变量的值,以便其他线程进入获取 monitor (锁)。

    由此看来,monitor 对象存在于每个 Java 对象的对象头中(存储的指针的指向),synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因,同时也是 notify()/notifyAll()/wait() 等方法存在于顶级对象 Object 中的原因。

    2.2 synchronized 代码块的原理

    可以通过对编译生成的代码反汇编来了解 synchronized 的作用原理,以下面的代码为例,

    public class SyncTest {
        static int i = 0;
        public static void main(String...args) {
            synchronized (SyncTest.class) {
                i++;
            }
        }
    }
    

    使用如下代码进行反汇编,javap -c ./SyncTest.class。反汇编结果,

    Compiled from "SyncTest.java"
    public class me.shouheng.test.SyncTest {
      static int i;
    
      static {};
        Code:
           0: iconst_0
           1: putstatic     #10                 // Field i:I
           4: return
    
      public me.shouheng.test.SyncTest();
        Code:
           0: aload_0
           1: invokespecial #15                 // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String...);
        Code:
           0: ldc           #1                  // class me/shouheng/test/SyncTest
           2: dup
           3: astore_1
           4: monitorenter                      // 获取锁
           5: getstatic     #10                 // Field i:I
           8: iconst_1
           9: iadd
          10: putstatic     #10                 // Field i:I
          13: aload_1
          14: monitorexit                       // 释放锁
          15: goto          21
          18: aload_1
          19: monitorexit                       // 发生异常时释放锁
          20: athrow
          21: return
        Exception table:
           from    to  target type
               5    15    18   any
              18    20    18   any
    }
    

    对照 Java 源代码和反汇编之后生成的代码。我们可以看出,同步语句块的实现使用的是monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

    当执行 monitorenter 指令时,当前线程将试图获取 monitor 的持有权,当 monitor 的计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 monitor 的持有权,那它可以重入这个 monitor,重入时计数器的值也会加 1。倘若其他线程已经拥有 monitor 的所有权,那当前线程将被阻塞。当 monitorexit 指令被执行,执行线程将释放 monitor (锁)并将计数器值减 1,当计数器为 0 时,释放锁,其他线程将有机会持有 monitor。

    值得注意的是编译器将会确保方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常时 monitorexit 指令依然可以地执行,编译器会自动产生一个异常处理器。这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个 monitorexit 指令,它就是异常结束时被执行的释放 monitor 的指令。

    2.3 synchronized 方法的原理

    方法级的同步是隐式的,无需通过字节码指令来控制。JVM 可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor,然后再执行方法,最后再方法完成 (无论是正常完成还是非正常完成) 时释放monitor. 在方法执行期间,其他任何线程都无法再获得同一个 monitor. 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的 monitor 将在异常抛到同步方法之外时自动释放。

    以下面的代码为例,

    public class SyncMethod {
        int x;
        public synchronized void syncMethod() {
            x++;
        }
    }
    

    要输出方法的信息,需要使用指令 javap -c -verbose ./SyncMethod.class,于是得输出,

      public synchronized void syncMethod();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
      ...
    

    ACC_SYNCHRONIZED 标识指明了该方法是一个同步方法。JVM 通过 ACC_SYNCHRONIZED 来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

    在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。Java 6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,锁效率也得到了优化。

    sychronized & ReentrantLock

    除了使用 sychronized,我们还可以使用 JUC 中的 ReentrantLock 来实现同步,它与 sychronized 类似,区别主要表现在以下 3 个方面:

    1. 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。ReentrantLock 使用 CAS 实现,可以在获取锁的时候设置一个超时的时间,当到达了指定的时间仍然没有获取到锁可以放弃获取锁。
    2. 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁无法保证,当锁被释放时任何在等待的线程都可以获得锁。sychronized 本身时非公平锁,而 ReentrantLock 默认是非公平的,可以通过构造函数要求其为公平的。
    3. 锁可以绑定多个条件:ReentrantLock 可以绑定多个 Condition 对象,而 sychronized 要与多个条件关联就不得不加一个锁,ReentrantLock 只要多次调用 newCondition 即可。

    参考

    相关文章

      网友评论

          本文标题:并发编程专题 3:synchronized

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