并发锁机制synchronized
注意:了解synchornized之前,必须要了解并发编程的三大特性:及时可见性,有序性,原子性,其中可见性,有序性可以通过java关键字 volatile 实现,volatile原理在下一篇文章中介绍
1.synchronized的两种用法
第一,修饰方法,通过ACC_SYNCHRONIZED标识。
第二,修饰方法体,通过monitorenter和monitorexit两个jvm指令实现
这里有两个moniterExit,第一个moniterexit是正常退出,第二个moniterexit是考虑到异常的情况。
2.Moniter(监视器/管程)
3.MESA模型
在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。
while(condition){
wait();
}
- 所有等待线程拥有相同的等待条件;
- 所有等待线程被唤醒后,执行同样的操作
- notify()只能唤醒一个线程
满足以上条件,可以使用notify,其余时候请使用notifyall();
4.java synchornized实现的管程
java参考MESA模型,自己内置了管程的实现synchornized,在MESA中,有多个的条件队列,在java实现的管程只有一个条件队列。器模型图如下
java对于Monitor的实现,主要在java.lang.Object中,在该类中定义了wait(),notify(),notifyall()方法,这些具体方法的实现,依赖于c++实现的ObjectMonitor类,
ObjectMonitor() { _header = NULL; //对象头 markOop _count = 0; _waiters = 0, _recursions = 0; // 锁的重入次数 _object = NULL; //存储锁对象 _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程) _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构) FreeNext = NULL ; _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程) _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0;
...
ObjectMonitor定义了三个队列
_WaitSet:等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点,
_EntryList:存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程);
_cxq:多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
其关系如下:
java线程唤醒额默认策略:
在获取锁时,是将当前线程插入到cxq的头部(栈结构头插法),而释放锁时,默认策略是:当EntryList为空,则将cxq中的元素按照原顺序插入到EntryList中,并且唤醒一个线程, 也就是当EntryList为空时,是后来的线程向北唤醒(因此,synchornized是非公平锁),ExtryList不为空时,从ExtryList中唤醒。
了解这个问题,首先需要学习对象在内存中的布局:
对象头详解
mark word:用于存储运行时数据,比如:hashcode,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit(8字节),官方称它为“Mark Word”。
Klass Pointer:即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。jdk1.8默认开启指针压缩功能,当堆内存<32G时,占4个字节,否则,占8个字节,所以一般建议堆内存不超过32G;
数组长度:数组对象独有
对象头的布局可以通过JOL(java Object layout)工具查看,比如:
- OFFSET:偏移地址,单位字节;
- SIZE:占用的内存大小,单位为字节;
- TYPE DESCRIPTION:类型描述,其中object header为对象头;
- VALUE:对应内存中当前存储的值,二进制32位;
64位系统对象头结构
下面通过JOL来追踪锁的变化过程:
- 无锁:对象在创建时是无锁的
-
- 偏向锁:偏向锁是针对锁的一种优化手段,为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁,比如StringBuffer的append方法,
- 偏向锁延迟偏向
jvm虚拟机默认在启动 4s后,为每个新建的对象开启偏向锁模式。
偏向锁在偏向过程中,调用对象hashcode,此时,将升级为重量锁
- 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
- 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。
偏向锁撤销之notify()/wait()
调用wait()