【多线程】synchronized背后的monitor锁

邓敏 2022年03月21日 53次浏览

monitor的作用

我们都知道synchronized的作用是用来保证修饰的代码或者方法执行有且只有一个线程执行,也就是锁。那么在执行被锁住的方式时,synchronized就需要通过monitor来记录和保证锁的状态。所以monitor这里的作用其实就是起到了控制synchronized什么时候获取锁,什么时候释放锁,以及记录了锁被重用的次数。

获取和释放monitor锁的时机

被修饰的同步代码块

对于被synchronized修饰的代码块,在生成class字节码文件中会出现monitorenter、monitorexit。如下面例子所示:

  public void synBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter #1
         4: getstatic                 
         7: ldc            
         9: invokevirtual            
        12: aload_1
        13: monitorexit #2
        14: goto          
        17: astore_2
        18: aload_1
        19: monitorexit #3
        20: aload_2
        21: athrow
        22: return

执行monitorenter的线程会尝试获取monitor的所有权,会发生以下三种情况之一:

  1. 如果该monitor的计数为0,这线程获得该monitor锁并设置为1
  2. 如果当前线程有了这个monitor锁,则该线程的monitor的计数累加1
  3. 如果其他线程尝试获取monitor锁,发现monitor的计数不为0,这表示当前线程被其他线程占用,则阻塞,直到这个monitor锁的计数变为0,然后再重新尝试获取。

执行monitorexit的线程就会将montior的计数减1,直到减到0为止,这时候就表示可以释放当前montior的锁了,其他的线程就可以尝试来获取当前代码的锁了。

看到这里,可能会有疑问,为什么生成的字节码文件中,一个monitorenter为什么对存在两个monitorexit,这里其实是考虑到代码发生了异常的情况,当我们在正常执行完任务之后,会执行#2的monitorexit去释放锁,但是出现异常了就会去执行#3的monitorexit的锁。这样就避免了死锁的发生,保证在任何情况下都能正常释放锁。

被修饰的同步方法

同步代码块是使用monitorenter和monitorexit来实现的,对于方法则不是依靠它两来实现的二十通过一个ACC_SYNCHRONIZED的flag修饰符,源代码如下:

  public synchronized void synMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 16: 0

当某个线程需要访问这个方法的时候,会先检查这个方法是否有ACC_SYNCHRONIZED这个标签,如果有就需要先获取monitor锁,其他的方面和同步代码块的逻辑是一样的。