• Java-JUC(十五):synchronized执行流程分析


    一、锁对象及 synchronized 的使用

    synchronized 通过互斥锁(Mutex Lock)来实现,同一时刻,只有获得锁的线程才可以执行锁内的代码。

    锁对象分为两种:

    实例对象(一个类有多个)和 Class 对象(一个类只有一个)。

    不同锁对象之间的代码执行互不干扰,同一个类中加锁方法与不加锁方法执行互不干扰。

    使用 synchronized 有以下种方式:

    修饰普通方法,锁当前实例对象。

    修饰静态方法,锁当前类的 Class 对象。

    修饰代码块,锁括号中的对象(实例对象或 Class 对象)。

    示例:

    class SynchronizedDemo {
        // 类锁(修饰静态方法:锁当前类的 Class 对象。)
        public static synchronized void inStaticMethod() {
            for (int i = 0; i < 10; i++) {
                System.out.println("aaa");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }    
        // 类锁(修饰代码块,锁括号中的 Class 对象)
        public static void inStaticMethodLockClassObj() {
            synchronized(SynchronizedDemo.class){
                for (int i = 0; i < 10; i++) {
                    System.out.println("aaa");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        // 对象锁(修饰普通方法:锁当前实例对象)
        public synchronized void inNormalMethod() {
            for (int i = 0; i < 10; i++) {
                System.out.println("bbb");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } 
        // 对象锁(修饰代码块:锁括号中的实例对象)
        public  void bb() {
            synchronized(this){
                for (int i = 0; i < 10; i++) {
                    System.out.println("bbb");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        // 无锁
        public void cc() {
            for (int i = 0; i < 10; i++) {
                System.out.println("ccc");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    View Code

    二、特性

    原子性

    被 synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在 Java 中可以使用 synchronized 来保证方法和代码块内的操作是原子性的。

    可见性

    对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。

    有序性

    synchronized 本身是无法禁止指令重排和处理器优化的,
    as-if-serial 语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。
    编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义。
    synchronized 修饰的代码,同一时间只能被同一线程执行。所以,可以保证其有序性。

    三、静态方法内部的代码块执行分析

    测试代码:

    public class DriverInstance {
        private static DriverInstance instance = null;
    
        private DriverInstance() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        public static DriverInstance getInstance() {
            if (instance == null) {
                synchronized (DriverInstance.class) {
                    System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->locking ");
                    if (instance == null) {
                        System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->begin set value ");
                        instance = new DriverInstance();
                        System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->end set value ");
                    }
                    System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->unlock ");
                }
            }
    
            return instance;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            CountDownLatch countDownLatch = new CountDownLatch(10);
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(DriverInstance.getInstance());
                        countDownLatch.countDown();
                    }
                }).start();
            }
    
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("first complete...");
    
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(DriverInstance.getInstance());
                    }
                }).start();
            }
        }
    }

    打印结果:

    1027791747517115-> Thread-0 ->locking 
    1027791747791125-> Thread-0 ->begin set value 
    1027792830581000-> Thread-0 ->end set value 
    1027792830905707-> Thread-0 ->unlock 
    1027792831314389-> Thread-9 ->locking 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792831593375-> Thread-9 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792831900975-> Thread-8 ->locking 
    1027792832238745-> Thread-8 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792832385236-> Thread-6 ->locking 
    1027792832555676-> Thread-6 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792832681639-> Thread-7 ->locking 
    1027792832829374-> Thread-7 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792832990484-> Thread-4 ->locking 
    1027792833090010-> Thread-4 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792833279111-> Thread-5 ->locking 
    1027792833473500-> Thread-5 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792833549389-> Thread-3 ->locking 
    1027792833643007-> Thread-3 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792833709254-> Thread-2 ->locking 
    1027792833797895-> Thread-2 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    1027792833875651-> Thread-1 ->locking 
    1027792833993217-> Thread-1 ->unlock 
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    first complete...
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
    com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e

    测试结果解读:

    1)多个线程是同时进入getInstance方法执行,在执行getInstance方法时(除了synchronized代码块以外的代码)线程之间是异步执行的;

    2)从上边测试结果可以看出,线程Thread-0优先进入了synchronized中(优先获取到了synchronized锁),这时其他9个线程都在排队等待进入synchronized中;

    3)当thread-0释放锁后,才其他线程排队依次执行“获取锁、初始化 instance、释放锁”。

    4)因为是第一次执行,多个线程中 instance 初始值都是null,因此当它们进入到第一个if(instance ==null),然后排队获取锁,上边测试结果中thread-0获取到锁后给 instance 赋值,赋值之后释放锁,释放锁的同时更新 instance 变量内存值(同时把所有的thread的本地副本变量刷新),当thread-0已经释放了锁后,队列中的等待获取锁的其他线程依次“获取到锁,进入锁内部代码执行第二个if(instance ==null)发现变量 instance 值已经不为空,不执行 instance 赋值操作,释放锁”。

    5)经过CountDownLatch的wait之后,重新启动的10个线程,此时这10个线程在初始化的时候 instance 的内存值不为空,每个线程赋值 instance 到本地线程,然后执行getInstalce,进度第一个if(instance==null)判断,发现不为空,直接返回 instance 变量,根本不会进入到锁内部。

    四、synchronized 的实现:monitor 和 ACC_SYNCHRONIZED

    synchronized的实现是基于monitor实现的,但是使用方式不同(修饰方法、修饰代码块),其内部实现有差别:

    同步代码块

    JVM 规范描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.14 

    使用 monitorenter 和 monitorexit 两个指令实现。

    每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。

    当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增(可重入性)。当同一个线程释放锁(执行 monitorexit)后,该计数器自减。当计数器为0的时候,锁将被释放。

    同步方法

    JVM 规范描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

    同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当线程访问时候,会检查是否有 ACC_SYNCHRONIZED,有则需要先获得锁,然后才能执行方法,执行完或执行发生异常都会自动释放锁。

    ACC_SYNCHRONIZED 也是基于 Monitor 实现的。

    举例分析

    来查看下具体的静态方法内部的静态代码块的一个示例:

    编写一个测试类DriverInstance.java:

    package com.dx.test;
    
    public class DriverInstance {
        private static DriverInstance instance = null;
    
        private DriverInstance() {
        }
        
        public static DriverInstance getInstance() {
            if (instance == null) {
                synchronized (DriverInstance.class) {
                    if (instance == null) {
                        instance = new DriverInstance();
                    }
                }
            }
    
            return instance;
        }
        
        public void test(){
            
        }
    }

    使用javac编译DriverInstance.java代码:

    E:work>javac DriverInstance.java

    生成DriverInstance.class

    使用javap -v DriverInstance.class反编译DriverInstance.class

    E:work>javap -v DriverInstance.class
    Classfile /E:/work/DriverInstance.class
      Last modified 2019-8-28; size 610 bytes
      MD5 checksum 73f4e1682a85c9a8cf854edfdd09261b
      Compiled from "DriverInstance.java"
    public class com.dx.test.DriverInstance
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
       #2 = Fieldref           #3.#22         // com/dx/test/DriverInstance.instance:Lcom/dx/test/DriverInstance;
       #3 = Class              #23            // com/dx/test/DriverInstance
       #4 = Methodref          #3.#21         // com/dx/test/DriverInstance."<init>":()V
       #5 = Class              #24            // java/lang/Object
       #6 = Utf8               instance
       #7 = Utf8               Lcom/dx/test/DriverInstance;
       #8 = Utf8               <init>
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               getInstance
      #13 = Utf8               ()Lcom/dx/test/DriverInstance;
      #14 = Utf8               StackMapTable
      #15 = Class              #24            // java/lang/Object
      #16 = Class              #25            // java/lang/Throwable
      #17 = Utf8               test
      #18 = Utf8               <clinit>
      #19 = Utf8               SourceFile
      #20 = Utf8               DriverInstance.java
      #21 = NameAndType        #8:#9          // "<init>":()V
      #22 = NameAndType        #6:#7          // instance:Lcom/dx/test/DriverInstance;
      #23 = Utf8               com/dx/test/DriverInstance
      #24 = Utf8               java/lang/Object
      #25 = Utf8               java/lang/Throwable
    {
      public static com.dx.test.DriverInstance getInstance();
        descriptor: ()Lcom/dx/test/DriverInstance;
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=0
             0: getstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
             3: ifnonnull     37
             6: ldc           #3                  // class com/dx/test/DriverInstance
             8: dup
             9: astore_0
            10: monitorenter
            11: getstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
            14: ifnonnull     27
            17: new           #3                  // class com/dx/test/DriverInstance
            20: dup
            21: invokespecial #4                  // Method "<init>":()V
            24: putstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
            27: aload_0
            28: monitorexit
            29: goto          37
            32: astore_1
            33: aload_0
            34: monitorexit
            35: aload_1
            36: athrow
            37: getstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
            40: areturn
          Exception table:
             from    to  target type
                11    29    32   any
                32    35    32   any
          LineNumberTable:
            line 10: 0
            line 11: 6
            line 12: 11
            line 13: 17
            line 15: 27
            line 18: 37
          StackMapTable: number_of_entries = 3
            frame_type = 252 /* append */
              offset_delta = 27
              locals = [ class java/lang/Object ]
            frame_type = 68 /* same_locals_1_stack_item */
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    
      public void test();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=0, locals=1, args_size=1
             0: return
          LineNumberTable:
            line 23: 0
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: aconst_null
             1: putstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
             4: return
          LineNumberTable:
            line 4: 0
    }
    SourceFile: "DriverInstance.java"

    备注:

    aload 指令的解释:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶

    aload_0把this装载到了操作数栈中aload_0是一组格式为aload_的操作码中的一个,这一组操作码把对象的引用装载到操作数栈中标志了待处理的局部变量表中的位置,但取值仅可为0、1、2或者3。

    还有一些其他相似的操作码用来装载非对象引用,包括iload_、lload_、fload_和dload_,这里的i代表int型,l代表long型,f代表float型以及d代表double型。在局部变量表中的索引位置大于3的变量的装载可以使用iload、lload、fload,、dload和aload,这些操作码都需要一个操作数的参数,用于确认需要装载的局部变量的位置。

    astore 指令的解释:将栈顶数值(objectref)存入当前frame的局部变量数组中指定下标(index)处的变量中,栈顶数值出栈。

    在monitorexit之前都会调用aload操作,实际上我们可以理解为“这里就是实现内存可见性实现的,在释放锁之前把变量同步回主存中”。

    五、锁获取和锁释放的内存语义

    线程A.B同时开始执行,获取主存中的x变量,x的变量初始值是0,线程A优先拿到锁,此时线程A在“同步代码块”或者“同步方法”内修改了x变量的值为1,当线程A释放锁之前会将修改x变量值刷新到主存中。
    整个过程即为线程A 加锁-->执行临界区代码-->释放锁相对应的内存语义。

    线程B获取锁的时候同样会从主内存中共享变量x的值,这个时候就是最新的值1,然后将该值拷贝到线程B的工作内存中去,释放锁的时候同样会重写到主内存中。

    从整体上来看,线程A的执行结果(a=1)对线程B是可见的,实现原理为:释放锁的时候会将值刷新到主内存中,其他线程获取锁时会强制从主内存中获取最新的值。

    从横向来看,这就像线程A通过主内存中的共享变量和线程B进行通信,A 告诉 B 我们俩的共享数据现在为1啦,这种线程间的通信机制正好吻合java的内存模型正好是共享内存的并发模型结构。

    六、Java对象如何与Monitor关联

    JVM堆中存放的是对象实例,每一个对象都有对象头,对象头里有Mark Word,里面存储着对象的hashCode、GC分代年龄以及锁信息。

    如图所示,重量级锁中存有指向monitor的指针。

    32位的HotSpot虚拟机对象头存储结构:

    为了证实上图的正确性,这里我们看openJDK--》hotspot源码markOop.hpp(新的定义类:MarkWord,hpp),虚拟机对象头存储结构:

    https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/markWord.hpp

     

    上图中有源码中对锁标志位这样枚举

    openJDK中ObjectMonitor类定义:

    https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/objectMonitor.hpp

     

    ObjectMonitor中几个关键字段的含义:
    _recursions:锁的重入次数。这句话很好理解,这也决定了synchronized是可重入的。
    _owner:指向拥有该对象的线程
    _WaitSet:主要存放所有wait的线程的对象,也就是说如果有线程处于wait状态,将被挂入这个队列,调用了wait()方法线程会进入该队列。
    _EntryList:所有在等待获取锁的线程的对象,也就是说如果有线程处于等待获取锁的状态的时候,将被挂入这个队列。

    对象,对象监视器,同步队列和线程状态的关系:

    图中描述内容解释:

    在Synchronized使用中,任意线程对Object的访问,首先要获得Object的监视器;
    如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED;
    当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

    实际上如果synchronized内部使用wait还会存在另外一种wait状态。

    示例:

    private static List<Integer> lists = new ArrayList<>();
    
     public static void main(String[] args) {
      final Object lock = new Object();
         //监控线程
         new Thread(()->{
             synchronized (lock) {
                 System.out.println("thread 2 start...");
                 if(lists.size() != 5) {
                     try {
                      System.out.println("thread 2 start wait");
                         lock.wait();
                         System.out.println("thread 2 end wait");
                     } catch (Exception e) { e.printStackTrace(); }
                 }
                 System.out.println("thread 2 start notify");
                 lock.notify();
                 System.out.println("thread 2 end notify");
             }
         }, "t2").start();
         
         new Thread(()->{
             synchronized (lock) {
                 for(int i = 0; i < 10; i++) {
                     System.out.println("thread 1, add " + i);
                     lists.add(i);
                     if(lists.size() == 5) {
                      System.out.println("thread 1 start notify");
                         lock.notify();
                      System.out.println("thread 1 end notify");
                         try {
                       System.out.println("thread 1 start wait");
                             lock.wait();
                          System.out.println("thread 1 end wait");
                         } catch (Exception e) { e.printStackTrace(); }
                     }
                     try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); }
                 }
             }
         }, "t1").start();
      
     }

    当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中,当某个线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 _owner 变量设置为当前线程,同时 monitor 中的计数器 _count 加 1。即获得对象锁。

    若持有 monitor 的线程调用 wait() 方法,将释放当前持有的 monitor,_owner 变量恢复为 null,_count 自减 1,同时该线程进入 _WaitSet 集合中等待被唤醒。

    若当前线程执行完毕也将释放 monitor(锁) 并复位变量的值,以便其他线程进入获取 monitor(锁)。

    示意图如下:

     

    参考:

    jdk源码剖析二: 对象内存布局、synchronized终极原理

    ava并发-深入理解synchronized

    Java-内存模型 synchronized 的内存语义

    让你彻底理解Synchronized

  • 相关阅读:
    MFC 简介
    C++使用thread类多线程编程
    C++中stack
    C++中头文件简介(stdio.h & chrono)
    别人写的很好Arduino教材
    Communicating to 2 SPI Slaves with USART & SPI ports on Atmega16U2
    HDU 2089 不要62(挖个坑=-=)
    HDU 3555 Bomb(数位DP)
    HDU 3480 Division(斜率优化+二维DP)
    HDU 3045 Picnic Cows(斜率优化DP)
  • 原文地址:https://www.cnblogs.com/yy3b2007com/p/11425542.html
Copyright © 2020-2023  润新知