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:如有不足,还望大佬斧正

原文地址:https://www.cnblogs.com/itcod/p/14613174.html