🍎1. 什么是Monitor?
我们都知道synchronized的作用是用来保证修饰的代码或者方法执行有且只有一个线程执行,也就是锁。那么在执行被锁住的方式时,synchronized就需要通过monitor来记录和保证锁的状态。所以monitor这里的作用其实就是起到了控制synchronized什么时候获取锁,什么时候释放锁,以及记录了锁被重用的次数。
🍎2. 当多线程时Monitor如何执行
前置知识了解
owner : 指向的是当前获得线程的地址,用来判断当前锁是被哪个线程持有。
waitSet : 是指已经获取得一次锁了,对象调用了wait方法,讲当前线程挂起了就进入了等待队列。等待时间到期的时候唤醒,或者其他线程唤醒。
entryList : 是队列用来获取锁的缓冲区,用来将cxq和waitSet中的数据 移动到entryList进行排队。这个统一获取锁的入口。一般是cxq 或者waitSet数据复制过来进行统一排队。
执行流程
刚开始Monitor中Owner为null
当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
在Thread-2上锁的过程中,如果Thread-3,Thread-.4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED(阻塞)
Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时是非公平的图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,
🍎3. 以为字节码举例
对于被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锁,其他的方面和同步代码块的逻辑是一样的。