zoukankan      html  css  js  c++  java
  • Java:Synchronized实现原理

    Java:Synchronized实现原理

    一、Synchronized实现同步代码块:

    先来看个两个简单的程序

    // 代码一
    public class OutOfSyncMonitor {
        private int data = 0;
        public void method1() {
                try {
                    data = data + 1;
              // 线程休眠1s,使其在还未执行完毕时,cpu进行时间片轮转
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
        }
    
        public static void main(String[] args) {
            final OutOfSyncMonitor monitor = new OutOfSyncMonitor();
            new Thread(monitor::method1).start();
            new Thread(monitor::method1).start();
        }
    }
    
    // 代码二
    public class OutOfSyncMonitor {
        private int data = 0;
        public void method1() {
            synchronized (this) {
                try {
                    data = data + 1;
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            final OutOfSyncMonitor monitor = new OutOfSyncMonitor();
            new Thread(monitor::method1).start();
            new Thread(monitor::method1).start();
        }
    }
    

    可以很容易的看出代码一是线程不安全的,代码二在method1方法中采用synchronized实现同步代码块保证了线程安全,我们再来看个代码

    // 代码三
    public class OutOfSyncMonitor {   
        // 所有的OutOfSyncMonitor的对象操作的多是同一个data
        private static int data = 0;
        public void method1() {
            synchronized (this) {
                try {
                    data = data + 1;
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            OutOfSyncMonitor monitor = new OutOfSyncMonitor();
            OutOfSyncMonitor monitor1 = new OutOfSyncMonitor();
            new Thread(monitor::method1).start();
            new Thread(monitor1::method1).start();
        }
    }
    

    在method1方法中还是采用synchronized实现同步代码块,期待保证数据安全,但是令人意外的时输出的data并不是我们所想要的,这样的实现并不能保证线程安全。为什么呢?我们来分析下

    synchronized是对同步代码块进行加锁,线程再去争抢锁来达到同步的目的。但是如果锁不唯一呢?换句话来说就是不是同一把锁呢?这显然达不到设计的初衷。而代码三就是这样的,在method1中锁的对象不唯一

    public static void main(String[] args) {
            OutOfSyncMonitor monitor = new OutOfSyncMonitor();
            OutOfSyncMonitor monitor1 = new OutOfSyncMonitor();
            new Thread(monitor::method1).start();        
            new Thread(monitor1::method1).start();
    }
    

    在这里创建了两个OutOfSyncMonitor对象,两个线程分别执行它们的method1方法,而在method1中synchronized的对象时this,导致了我们非期望的效果。为什么会出现这种情况呢?我们看下线程的堆栈信息(jstack )

    "Thread-1" #13 prio=5 os_prio=0 tid=0x0000022a97b14000 nid=0x2d60 waiting on condition [0x000000e8343ff000]
       java.lang.Thread.State: TIMED_WAITING (sleeping)
            at java.lang.Thread.sleep(Native Method)
            at java.lang.Thread.sleep(Thread.java:340)
            at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
            at cn.itcod.thread.OutOfSyncMonitor.method1(OutOfSyncMonitor.java:14)
            - locked <0x000000076b933780> (a cn.itcod.thread.OutOfSyncMonitor)
            at cn.itcod.thread.OutOfSyncMonitor$$Lambda$2/1096979270.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:748)"Thread-0" #12 prio=5 os_prio=0 tid=0x0000022a97b10000 nid=0x3cf8 waiting on condition [0x000000e8342ff000]
       java.lang.Thread.State: TIMED_WAITING (sleeping)
            at java.lang.Thread.sleep(Native Method)
            at java.lang.Thread.sleep(Thread.java:340)
            at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
            at cn.itcod.thread.OutOfSyncMonitor.method1(OutOfSyncMonitor.java:14)
            - locked <0x000000076b933770> (a cn.itcod.thread.OutOfSyncMonitor)
            at cn.itcod.thread.OutOfSyncMonitor$$Lambda$1/1324119927.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:748)
    

    从上可以看出每个线程争抢的monitor关联引用是彼此独立的,这也就导致了锁失败的原因。可以采取以下方法来实现我们的预期效果

    public class OutOfSyncMonitor {
        private static int data = 0;
        public void method1() {
            synchronized (OutOfSyncMonitor.class) {
                try {
                    data = data + 1;
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }  
        public static void main(String[] args) {// 省略}
     }
    

    我们通过锁类来实现同步代码块。这就有两个概念:对象锁 和 类锁

      1、对象级别的锁是锁定在对象上的一把锁,只有在线程在访问的是同一个对象时,才会通过竞争来获取得锁。

      2、类级别的锁是锁定在类上的一把锁,当线程执行这类的方法时,不管调用的对象是否是同一个,多会产生锁竞争来获得锁。

    二、Synchronized的实现原理
    我们使用javap对代码一和代码二进行反汇编看下JVM指令:

    public void method1();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=4, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter
             4: aload_0
             5: aload_0
             6: getfield      #2                  // Field data:I
             9: iconst_1
            10: iadd
            11: putfield      #2                  // Field data:I
            14: getstatic     #3                  // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
            17: lconst_1
            18: invokevirtual #4                  // Method java/util/concurrent/TimeUnit.sleep:(J)V
            21: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
            24: aload_0
            25: getfield      #2                  // Field data:I
            28: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
            31: goto          39
            34: astore_2
            35: aload_2
            36: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V
            39: aload_1
            40: monitorexit
            41: goto          49
            44: astore_3
            45: aload_1
            46: monitorexit
            47: aload_3
            48: athrow
            49: return
    

    我们可以看到代码二的反汇编第9行和第40行有两个特别的指令monitorenter和monitorexit,并且是成对出现的(有些时候会出现一个monitorenter多个monitorexit,但是每一个monitorexit之前必有对应的monitor enter,这是肯定的。【Java高并发编程详解:汪文君】);在不加锁的代码一中却没有出现这两个指令。

    public void method1();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=2, args_size=1
             0: aload_0
             1: aload_0
             2: getfield      #2                  // Field data:I
             5: iconst_1
             6: iadd
             7: putfield      #2                  // Field data:I
            10: getstatic     #3                  // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
            13: lconst_1
            14: invokevirtual #4                  // Method java/util/concurrent/TimeUnit.sleep:(J)V
            17: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
            20: aload_0
            21: getfield      #2                  // Field data:I
            24: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
            27: goto          35
            30: astore_1
            31: aload_1
            32: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V
            35: return
    

    这是synchronized关键字包裹monitor enter和monitor exit两个JVM指令,它能够保证在如何时候线程执行到monitor enter成功之前必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功后,共享变量被更新后的值必须刷入主内存。下面来讲下这两个指令
      1、monitor enter
    每个对象多有与monitor与之关联,一个monitor的lock锁只能被一个线程在同一时间获取,在一个线程尝试获取与锁对象相关联的monitor时会发生以下几件事情。
    如果monitor的计数器为0,意味着该monitor的lock锁还没有被获取,当一个线程获的后会立刻对该计数器+1,这样就代表这该monitor被占有
    如果一个已经拥有该monitor所有权的线程重入,则会导致monitor的计数器再次被累加
    如果monitor已经被其他线程占有,其他线程尝试获取该monitor的所有权时,被陷入到阻塞状态,知道monitor计数器变为0,才再次尝试获取monitor所有权
      2、monitor exit
      释放对monitor的所有权,前提是曾经获得过所有权。释放的过程较为简单,就是将monitor的计数器-1,如果计数器的结果为0。则代表这线程失去了对该monitor的所有权,与此同时被该monitor block的线程将再次尝试获取该monitor的所有权。

    PS:如有不足,还望大佬斧正

  • 相关阅读:
    Binary Search Tree Iterator 解答
    Invert Binary Tree 解答
    Min Stack 解答
    Trapping Raining Water 解答
    Candy 解答
    Jump Game II 解答
    Implement Hash Map Using Primitive Types
    Gas Station 解答
    Bucket Sort
    HashMap 专题
  • 原文地址:https://www.cnblogs.com/itcod/p/14613174.html
Copyright © 2011-2022 走看看