synchronized背后的“monitor锁”和Lock的比较

  • Post author:
  • Post category:其他



前言


在前面文章

synchronized关键字的四种加锁方式

中介绍了四种synchronized的使用和区别,但是效果都是一样的,今天我们更加深入的看一看synchronized背后的’’monitor”锁,以及和Lock的区别。



synchronized背后的’’monitor”锁

synchronized的使用非常简单,仅需要在方法或者代码块中使用synchronized就可以了,使用看起来似乎很简单的背后是Java团队背后为我们做了很多的努力来简化我们的使用。synchronized如何使用直接影响了程序的效率。

因为每一个Java对象内部都只有一个锁,如果使用synchronized使一个线程获取了某一个对象的锁之后,其它线程就无法获取这个对象的锁了,因为只有一个锁,其他线程只能等这个线程释放这个锁。


首先我们看synchronized在同步代码块中的使用背后的字节码

,源码如下:

public class TestSynchronized {

    public void synMethod(Thread thread) {
        synchronized (this) {

        }
    }

    public void synMethod1(Thread thread) {
        synchronized (TestSynchronized.class) {

        }
    }
	
	public void synMethod2(Thread thread) {

    }
}    
  • 这里有两个使用同步代码块加锁的方法,我们使用cmd命令切换到类

    TestSynchronized

    对应的目录下,使用编译命令

    javac TestSynchronized.java

    ,这时候会出现一个编译后的字节码文件

    TestSynchronized.class

    ,使用命令

    javap -verbose TestSynchronized.class

    就可以查看字节码文件 如下:
Classfile /D:/develop/androidstudio/Forward/app/src/main/java/com/oman/forward/study/TestSynchronized.class
  Last modified 2020-3-29; size 614 bytes
  MD5 checksum a93f484683fe2ce446f688c977d73f1a
  Compiled from "TestSynchronized.java"
public class com.oman.forward.study.TestSynchronized
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#19         // java/lang/Object."<init>":()V
   #2 = Class              #20            // com/oman/forward/study/TestSynchronized
   #3 = Class              #21            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               synMethod
   #9 = Utf8               (Ljava/lang/Thread;)V
  #10 = Utf8               StackMapTable
  #11 = Class              #20            // com/oman/forward/study/TestSynchronized
  #12 = Class              #22            // java/lang/Thread
  #13 = Class              #21            // java/lang/Object
  #14 = Class              #23            // java/lang/Throwable
  #15 = Utf8               synMethod1
  #16 = Utf8               synMethod2
  #17 = Utf8               SourceFile
  #18 = Utf8               TestSynchronized.java
  #19 = NameAndType        #4:#5          // "<init>":()V
  #20 = Utf8               com/oman/forward/study/TestSynchronized
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/Thread
  #23 = Utf8               java/lang/Throwable
{
  public com.oman.forward.study.TestSynchronized();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

  public void synMethod(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: dup
         2: astore_2
         3: monitorenter
         4: aload_2
         5: monitorexit
         6: goto          14
         9: astore_3
        10: aload_2
        11: monitorexit
        12: aload_3
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any
      LineNumberTable:
        line 11: 0
        line 13: 4
        line 14: 14
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class com/oman/forward/study/TestSynchronized, class java/lang/Thread, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public void synMethod1(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: ldc           #2                  // class com/oman/forward/study/TestSynchronized
         2: dup
         3: astore_2
         4: monitorenter
         5: aload_2
         6: monitorexit
         7: goto          15
        10: astore_3
        11: aload_2
        12: monitorexit
        13: aload_3
        14: athrow
        15: return
      Exception table:
         from    to  target type
             5     7    10   any
            10    13    10   any
      LineNumberTable:
        line 17: 0
        line 19: 5
        line 20: 15
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 10
          locals = [ class com/oman/forward/study/TestSynchronized, class java/lang/Thread, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public void synMethod2(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 24: 0
}
SourceFile: "TestSynchronized.java"
  • 我们重点关注synMethod和synMethod1,他们使用的是同步代码块加锁的方式,我们看到他们里面比synMethod2普通的方法多了两个指令,分别是monitorenter 和 monitorexit 指令,而且有两个monitorexit指令,之所以有两个monitorexit指令的原因是monitorenter只需要插入到同步代码块开始的位置,而monitoreexit需要在同步代码块正常结束和异常的位置都插入,这样就可以在异常的时候也释放锁。
  • 每个对象中都有一个维护着被锁次数的计数器,monitorenter代表计数器加1,monitorexit代表计数器减1,如果计数器的值为0的话,就代表着这个对象未被线程使用。

  • monitorenter

    代表开始加锁,意味着执行monitorenter的线程开始尝试获取对象的monitor的所有权,加锁分为几种情况:

    • 如果对象中monitor的计数器为0,代表此对象没有被使用,则执行monitorenter的线程就可以占有这个对象的monitor。
    • 如果对象中monitor的计数器不为0,并且此线程已经持有此对象的monitor的话,那么monitor计数器就累计加1。
    • 如果对象中的monitor计数器不为0,并且其他线程已经持有了此对象的monitor的话,那么此线程就处于BLOCKED状态,需要等待这个monitor计数器的值为0,值为0意味着这个monitor已经被释放了,那么其它等待这个monitor的线程就可以再次尝试获取monitor的所有权了。

  • monitorexit

    意味着将对象中的monitor计数器减1,上面分析了,当计数器的值为0的时候意味着这个monitor已经被释放了,那么其它等待这个monitor的线程就可以再次尝试获取monitor的所有权了。


接下来我们看synchronized在同步方法中使用背后的字节码

,源码如下:

public class TestSynchronized {

    public void synMethod2(Thread thread) {

    }

    public synchronized void synMethod3(Thread thread) {

    }

    public static synchronized void synMethod4(Thread thread) {

    }
}
  • 这里有两个使用同步方法加锁的方法,我们还是和上面的命令一样,使用cmd命令切换到类

    TestSynchronized

    对应的目录下,使用编译命令

    javac TestSynchronized.java

    ,这时候会出现一个编译后的字节码文件

    TestSynchronized.class

    ,使用命令

    javap -verbose TestSynchronized.class

    就可以查看字节码文件 如下:
Classfile /D:/develop/androidstudio/Forward/app/src/main/java/com/oman/forward/study/TestSynchronized.class
  Last modified 2020-3-29; size 409 bytes
  MD5 checksum cf108dae480c6b6d58d6cfc8bb7a9c01
  Compiled from "TestSynchronized.java"
public class com.oman.forward.study.TestSynchronized
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#14         // java/lang/Object."<init>":()V
   #2 = Class              #15            // com/oman/forward/study/TestSynchronized
   #3 = Class              #16            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               synMethod2
   #9 = Utf8               (Ljava/lang/Thread;)V
  #10 = Utf8               synMethod3
  #11 = Utf8               synMethod4
  #12 = Utf8               SourceFile
  #13 = Utf8               TestSynchronized.java
  #14 = NameAndType        #4:#5          // "<init>":()V
  #15 = Utf8               com/oman/forward/study/TestSynchronized
  #16 = Utf8               java/lang/Object
{
  public com.oman.forward.study.TestSynchronized();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

  public void synMethod2(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 24: 0

  public synchronized void synMethod3(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 29: 0

  public static synchronized void synMethod4(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 33: 0
}
SourceFile: "TestSynchronized.java"
  • 我们重点关注synMethod3和synMethod4,我们看到他们两个和synMethod2的主要区别在于多了一个flag为

    ACC_SYNCHRONIZED

    的标记,这个标记用来标识它是同步方法的。而synMethod3和synMethod4的区别则是synMethod4比synMethod3多了一个

    ACC_STATIC

    的静态标识符。


小结

  • 使用synchronized修饰的方法会有一个

    ACC_SYNCHRONIZED

    标识,当此方法在准备执行的时候,会发现此方法包含

    ACC_SYNCHRONIZED

    标记,那么就需要先获取monitor锁(如果是非静态的话,是对象monitor,如果修饰的是静态方法则是类monitor),获取monitor之后才能执行方法体中的内容,方法执行结束后会释放monitor。
  • 使用synchronized修饰同步代码块的话,会有两个标识,monitorenter和monitorexit。monitorenter 代表开始加锁,意味着执行monitorenter的线程开始尝试获取对象的monitor的所有权,获取到monitor遇到的几种情况上面已经详细分析了,当同步代码块执行结束后,就会进入monitorexit释放锁。
  • 上面分析得知synchronized如果对于方法加锁的话,因为同步的范围过于大,将会影响程序的性能,所以在编程中可以考虑使用同步代码块来替代同步方法,这样对程序的性能会有提升。



synchronized和Lock的区别

synchronized和Lock都是用来保证线程安全的,下面我们比较它们主要的相同点和不同点。


  • 相同点:


    • synchronized和Lock都是用来保证线程安全的

      : Java并发编程的三要素包括原子性,可见性,有序性。因为使用synchronized和Lock在同一个时间,只能有一个线程执行代码,所以就保证了原子性,可见性和有序性,也就保证了线程安全。

    • synchronized和Lock都是可重入锁

      :可重入锁指的是如果线程获得了某一个对象的monitor,当再次试图获取这个对象的monitor的时候,是不需要先释放之前的monitor的,上面我们分析synchronized说到monitorenter的时候,说到了monitor计数器可以累加,说明synchronized是可重入锁,Lock也是可重入锁。

  • 不同点:


    • 灵活性不同

      : lock可以使用tryLock(time)等方法,如果获取不到锁的时候,可以去做其它的事情。而synchronized只能选择等待或者异常。

    • 加锁的显示和隐式:

      Lock必须显示的执行加锁和解锁,为了防止发生死锁,解锁一般在finally中执行。而synchronized的加锁和解锁是Java虚拟机内部实现的,通过上面的class字节码分析,本质上也是加锁和解锁的操作,并且monitorexit有两次,所以能够保证异常释放锁,只不过这些内容在代码上是不像Lock直接体现的。

    • 加锁解锁的顺序:

      synchronized的加锁和解锁,是按顺序出现的,而Lock可以加多个所锁,但是解锁不用按照顺序,可以把先加的锁后解。比如下面的代码:

      	//synchronized 有顺序要求
      	synchronized(obj1) {
      		synchronized(obj1) {
      			//do something
      		}	
      	}
      	lock无顺序要求
      	lock1.lock();
      	lock2.lock();
      	// do something
      	lock2.unlock();
      	lock1.unlock();
      

    • 被占有的线程个数

      :因为每个对象只有一个monitor,所以synchronized只能被一个线程占有。而lock没有这个限制。比如ReentrantReadWriteLock可以同时被多个线程持有读锁。

    • 公平性:

      Lock可以设置公平锁和非公平锁,比如ReentrantLock默认是不公平锁,可以通过构造方法设置公平锁,而synchronized不能设置。


小结

对synchronized和Lock说了这么多,如何选择呢,在我看来有以下几条建议:


  • 从效率考虑:

    Java在包java.util.concurrent下为我们提供了很多的并发类,如果使用的话首先考虑使用这些类(前提是足够了解这些类),比如考虑

    ConcurrentHashMap等替换

    HashMap



  • 安全性上考虑:

    因为synchronized使用简单,而且虚拟机内置帮我们处理了解锁的操作,就算是异常情况下也会解锁,所以从编码上来看的话,synchronized比Lock更加安全(因为Lock可能会忘记在finally中解锁)。

  • 从灵活性上考虑:

    Lock有更多的API可供更复杂的需求选择,如果你需要比如超时功能,可以考虑使用Lock。



版权声明:本文为oman001原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。