读前必知
文中锁,也称为对象锁,而锁对象就是指的承载这个锁的对象,如下面,用法中所指的Object o,在print3中就是锁对象。
以下源码分析仅适用于jdk8,版本未知,因为源码提供者的源码版本访问地址出错了
https://github.com/farmerjohngit/myblog/issues/12 源码提供者地址,配合源码服用更不容易懵
用法
我们都知道synchronized,可以加在对象或者方法头上
1 加在对象头上,那么这个对象就无法被再次上锁。
2 加在非静态方法头上,等同于加在当前对象头上。
3 加在静态方法头上,等同于加在当前对象的类对象头上。
**注意:**加在类对象头上并不会导致,这个类的任何实例对象无法上锁。即Class对象上锁与对象上锁无关。
public class Test {
public Object o = new Object();
public synchronized void print1(){
System.out.println("进入");
}
public synchronized static void print2(){
System.out.println("进入");
}
public void print3(){
synchronized (o){
System.out.println();
}
}
public static void main(String[] args) {
}
}
字节码
这是三个print的字节码结构,我们从中可以得出两点
1 如果方法使用了synchronized,那么它的flag会增加ACC_SYNCHRONIZED标识
这是因为ACC_SYNCHRONIZED可以提醒方法在进入之前获取相应的对象锁,在返回前释放对象锁。
2 如果使用了synchronized代码块,那么会出现一次monitorenter和两次monitorexit,这是为了防止异常返回而造成锁未释放。如图,如果正常执行,那么17行将会跳转到25行直接完成return,而如果不正常执行,那么7-17行如果出错,就会前往20行,将栈顶元素清空,然后放入锁对象,再执行monitorexit,再完成return。
获取锁流程
初始化
如果这个对象锁被获取过,并且没有释放,那么会从线程栈底到正在获取锁的lock record间寻找一个空闲的的lock record,找不到,会扩建栈,然后循环,直到上面那个获取结束。
如果这个对象锁没有被获取过,那么会从线程栈底到栈顶寻找一个空闲的lock record,用于后续操作。
这里有三点需要注意,一,获取的空闲lock record会是最接近顶的那个,这样可以避免第一种空耗情况的发生。二,无论是哪种情况,如果找不到空闲的lock record,都会给栈分配一个新的lock record空间。三,这个栈不是虚拟机栈也不会是操作数栈,虚拟机栈放的是方法,操作数栈放的是正在执行的操作数,都是一直需要在使用的结构,和这个栈无法共,但这个栈一定是线程私有的。
逐一查询为这次锁获取一个没有使用的lock record,如果没有找到,那么会生成一个全新的lock record存放于栈中,然后将这个lock record指向锁对象,正式进入锁的判断流程。
重入锁这样做的好处有两个,一,保证了最后一个被释放的就是第一次进入的锁,二,避免了过多向上遍历导致性能过低。
偏向锁
进入判断,如果自己是锁拥有者,则直接进入锁内代码块。如果不是自己,会尝试一下将threadId替换进去对象头的markword,如果尝试失败,就会进入锁升级流程,会在线程安全点尝试撤销偏向锁,并将锁对象改成轻量级锁,如果尝试成功,说明是一个匿名偏向锁,直接进入锁内代码块。
轻量级锁
会将锁对象的无锁状态mark word记录在自己displaced markword,如果发现锁对象恰好就是无锁状态,那么让锁对象markword前部分改为指向自己的指针(后面的留作标识为轻量级锁),如果不是,则证明着存在竞争,那么会进入重量级锁。
重量级锁
首先会将markword改为inflating模式,这是一条cmpxchg_ptr指令,保证了只有一个线程可以完成这个步骤。在这个模式下,所有后入线程都会进入循环等待,直到修改inflating模式的线程完成了monitor的初始化工作,然后取得monitor返回。完成初始化的线程也是返回monitor。
于是所有线程都会使用monitor.enter()来获取锁。
重量级锁时会维护一个monitor容器,里面主要存放着cxq,waitset和entrylist三种数据结构,其中head属性存放着自己的displaced markword,没错就是无锁那个,owner存放着定位到拥有者的句柄,可以是thread指针,markword中存放的lock record地址指针。然后将锁对象的mark word改为指向monitor。cxq是一个单向队列,entrylist是一个双向队列。
如果获取失败,那么会将自己放入cxq队列头部,如果获取成功,则会将自己从cxq或者entrylist队列中删除。
如果在同步块中调用了wait(),那么会被放入waitset,如果被notify,那么会放入entrylist。
释放锁流程
最初的开始
遍历整个虚拟机栈,找到所有指向该锁对象的lock record,将每个符合的lock record逐个指向设置为null,进行锁释放,释放流程如下
偏向锁释放
检查一下mark word最后是否是偏向位,如果是,直接返回,只需要lock record设置指向null过就行了。
轻量级锁释放
如果Displaced Mark Word
为null,说明是一个重入锁,直接返回,如果不是null,那么就比较mark word
是否指向自己。
如果指向自己的话,将Displaced Mark Word
替换过去,然后返回,不是的话,那就只剩下一种情况,线程持有锁的过程中,其它线程将锁膨胀成了重量级锁。
重量级锁の释放,公平与非公平?
进入重量级锁,如果自己不是锁的持有线程,那么会判断锁的owner是否指向了自己的lock record,如果是,那么会将自己也改为重量级锁,然后进入唤醒。需要注意的是,这时候并不需要考虑重入,因为走到这一步Displaced Mark Word
一定不是null,能走到这一步的一定是第一个获取到锁的那个lock record。
重量级锁的唤醒策略非常多,由一个Knob_QMode
的参数决定,参数为2,那么会优先唤醒cxq里的元素,成功直接返回。
参数为3,会将cxq的元素放入entrylist队尾,如果参数为4,那么会将cxq元素放入entrylist队头。
在此之后,唤醒entrylist队头元素。
看上去选择了参数3就可以做到了公平锁真是这样吗?不然。
首先,锁释放流程中,第一步就是释放锁,然后再完成后续唤醒流程,这给了后面获取锁的线程直接占有的机会。
其次,参数3的放入非常粗暴,把这个cxq单链表直接增加了prev指针,变成了双向链表,然后把cxq头节点(也就是最后进入的那个)的prev指针指向了entrylist尾节点,这样做的好处是,直接使用了现有结构,不需要额外空间。
从中我们也可以发现开发者的一个恶趣味,明明可以让cxq最先插入的节点和entrylist尾节点进行连接,但是却要让cxq最后插入的节点与其连接,相当于把整个cxq的顺序都在entrylist中反过来了,把不公平性发挥到了极致。
以下也提供了尾插代码供观赏。