【多线程】synchronized背后的monitor锁
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的所有权,会发生以下三种情况之一:
- 如果该monitor的计数为0,这线程获得该monitor锁并设置为1
- 如果当前线程有了这个monitor锁,则该线程的monitor的计数累加1
- 如果其他线程尝试获取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锁,其他的方面和同步代码块的逻辑是一样的。