1. Synchronized的各种使用方式 以及 锁的都是什么:
Synchronized方法,且为非静态方法:锁的是当前类的实例对象(this)。
Synchronized方法,但是为静态方法:锁的是当前类对象。
Synchronized块(Object):锁的是Object对象。
Synchronized块(this):锁的是当前类的实例对象。
Synchronized块(类.class):锁的是当前类对象。
2. 从字节码看Synchronized的实现原理:monitor
2.1 synchronized方法经过编译生成的字节码:
可以看到synchronized方法的flags属性为ACC_SYNCHRONIZED
synchronized void hy () { }
synchronized void hy(); descriptor: ()V flags: ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 5: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this LTest27JavapSycnhronized;
2.2 synchronized块经过编译生成的字节码:
可以看到synchronized块所在的方法的Code区多了一对monitorenter和monitorexit命令:
void hy6 (Hy h) { synchronized (h) { doSomething(); }
1 void hy6(Hy); 2 descriptor: (LHy;)V 3 flags: 4 Code: 5 stack=2, locals=4, args_size=2 6 0: aload_1 //将对象h从局部变量表入操作数栈(局部变量表的_1=对象h) 8 1: dup // 将栈顶的对象h复制并再入栈 9 2: astore_2 // 将对象h(新复制来的)加载入局部变量表,意味这是新元素(栈中的新h ——>成为 局部变量表的_2) 12 3: monitorenter // 锁住当前栈顶元素(即老对象h)的monitor, 13 4: aload_0 // 将局部变量表中的this入栈,目的是下一步执行dosomething方法时可以从栈顶取到this,因为this是dosomething方法 的入参。(局部变量表的_0=this) 18 5: invokevirtual #4 // 调用常量池的#4 ,即dosomething方法 19 8: aload_2 // 将局部变量表中的新h入栈(局部变量表的_2=新h) 21 9: monitorexit // 退出(即老对象h)的monitor, 22 10: goto 18 // 既然已经退出,说明是正常结束,取到#18行return掉。 24 13: astore_3 // 走到这里13行说明已经是异常了,因为查看异常表 Exception table得知,4~10发生的异常会走到13行(栈中的异常对象 ——>成为 局部变量表的_3) 28 14: aload_2 // 将局部变量表中的新h入栈(局部变量表的_2=新h) 30 15: monitorexit // 退出(即老对象h)的monitor, 31 16: aload_3 // 将局部变量表中的异常对象入栈(局部变量表的_3=异常对象) 33 17: athrow // 抛出异常 34 18: return // 返回 35 36 Exception table: 37 from to target type 38 4 10 13 any 39 13 16 13 any
2.3 总结一下
分别写一个Synchronized方法和Synchronized块,观察javap反编译后的字节码:
Synchronized方法(不论静态非静态)利用的机制是:
在所在方法的flags属性会多了一个参数:ACC_SYNCHRONIZED。
Synchronized块(锁的不论对象还是class)利用的机制是:
在所在方法的Code域中多了两个动作:monitorenter和monitorexit。
2.4 ACC_SYNCHRONIZED 和 monitorenter&monitorexit 有什么区别:
其实两者的底层实现是一样的,只不过ACC_SYNCHRONIZED是隐式的。
1. ACC_SYNCHRONIZED:
当虚拟机执行该方法时,看到该方法的flags之一是ACC_SYNCHRONIZED,
就会要求当前线程尝试获取当前操作数栈顶元素的monitor(如果flags还有ACC_STATIC,就获取当前调用者类对象的monitor),然后才能执行方法。
2. monitorenter&monitorexit:
执行monitorenter字节码指令时,当前线程尝试获取当前操作数栈顶元素的monitor,
执行monitorexit字节码指令时,退出那个元素的monitor。
3. Monitor
3.1 Monitor简介
每个对象有一个monitor监视器。
monitor可以实现Synchronized、wait / notify。
这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
执行monitorenter时,当前线程尝试获取当前操作数栈顶元素的monitor:
1、如果发现此时monitor的进入数(_count字段)为0,则该线程成功进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3、如果发现此时其他线程已经占用了monitor,则该线程进入阻塞状态。
执行monitorexit时,当前线程尝试退出那个元素的monitor,(前提是当前线程必须是那个元素的monitor所有者)
1、monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。
其他被这个monitor阻塞的线程(monitor的_EntryList字段)可以尝试去获取这个 monitor 的所有权。
3.2 Monitor详细介绍
ObjectMonitor中有几个关键属性:
_owner:指向持有ObjectMonitor对象的线程
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
_WaitSet:如果一个线程在同步块中调用了wait方法,会将该线程加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的线程从WaitSet移动到EntryList中
_cxq(ContentionList):当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程插入到cxq队列的队首
_EntryList:当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程
3.2.1 以Synchronized场景举例说明monitor的线程阻塞顺序和策略
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程插入到cxq队列的队首
3.2.2 以Synchronized场景举例说明monitor的线程唤醒顺序和策略
当一个线程执行到monitorexit时,会执行唤醒策略:
如果QMode = 2的操作最特殊:取_cxq队列首元素唤醒;
如果QMode = 3,把_cxq队列的首元素放入_EntryList尾部(相当于顺序),然后执行步骤5;
如果QMode = 4,把_cxq队列的首元素放入_EntryList头部(相当于倒序),然后执行步骤5;
如果QMode = 0(默认),不做什么,执行步骤下一步:
如果_EntryList非空,就取首元素+调用ExitEpilog方法,该方法会唤醒该元素对应的线程,然后立即返回;
如果_EntryList为空,会将cxq中的所有元素移动到EntryList中去+调用ExitEpilog方法,唤醒EntryList的队首线程,该方法会唤醒该元素对应的线程,然后立即返回;
假设线程A、B、C的执行顺序为 A->B->C,并且A线程首先抢到了锁:
因为锁被A线程占用,则会将B、C线程插入到cxq队列中,由于每次都是插入的队首,所以cxq队列中的元素顺序为C->B,
又因为EntryList为空,所以当A线程执行完毕后,则会取出C线程进行执行。
3.2.3 monitor实现wait/notify应该和实现Synchronized的方式差不多,只不过cxq队列换成_WaitSet队列而已
略。