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

前言
在前面文章 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。
synchronizedlock 都是 Java 中用于实现线程同步的机制,它们的作用都是保证在同一时刻只有一个线程能够访问共享资源。它们的原理有一些不同。 synchronizedJava 语言中的关键字,它是在 JVM 层面上实现的。它的机制是,每个 Java 对象都有一个监视器monitor),当一个线程访问该对象的 synchronized 代码块时,会尝试获取该对象的监视器,如果获取到,则表示该线程可以进入 synchronized 代码块执行,其他线程则必须等待。当 synchronized 代码块执行完毕时,该线程会释放,其他线程则可以尝试获取并进入 synchronized 代码块执行。 lockJava 5 中引入的一个接口,它提供了比 synchronized 更加灵活和强大的线程同步机制。lock 接口的实现类可以实现不同的定策略,比如可重入、公平、读写等。lock 接口中最常用的实现类是 ReentrantLocklock 的机制是,一个线程在访问共享资源前,必须先获取,如果获取不到,则该线程会被阻塞,直到获取到才能进入临界区访问共享资源。当线程访问完共享资源后,必须释放,其他线程才能获取访问共享资源。 总的来说,synchronizedJava 的内置机制,使用起来比较简单,但是定的粒度比较大,只能定整个对象,不能灵活控制定范围。而 lock 则是一个接口,使用起来比较灵活,可以实现不同的定策略,但是也比较复杂,需要手动加和解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值