- sync到底是对什么加锁
- Monitor模型
2.1 为什么sync是非公平锁
2.2 Monitor和对象头的关系
2.3 Monitor的JDK源码 - sync各种锁与Monitor的关系
3.1 锁膨胀过程
3.2 自适应式自旋
3.3 锁消除
3.4 锁粗化 - wait和notify方法的疑问
4.1 wait和notify为什么必须synchronized中使用
4.2 wait和notify为什么是Object的方法
1. sync到底是对什么加锁
synchronized锁静态方法和实例方法有什么区别?
JAVA虚拟机给每个对象和class字节码文件都设置了一个监控器Monitor,用于检测并发代码的重入。同时,Object类中提供notify和wait方法来对Monitor中的线程进行控制。
sync锁是一个可重入的非公平独占锁。sync加锁(此处特指重量级锁)会去获取obj的Monitor,如果Monitor已经被其他线程获取,那么当前线程会进入Entry Set
。等待其他线程释放obj的Monitor。
而这里的Monitor可以是类.class的Monitor,也可以是当前对象(this)Monitor。
也就是可以回答上面的问题:锁实例方法是同一个对象互斥,锁静态方法是全局互斥。
- 作用于实例方法时,锁住的是this对象为锁的所有代码块;
- 作用于静态方法时,锁住的是Class实例,又因为Class相关数据存储在永久代,永久代是全局贡献,因此静态方法相当于类的一个全局锁,会锁住调用该方法的所有线程;
- 作用于一个对象实例时,锁住的是所有以该对象为锁的所有代码块;
即sync借助obj中的Monitor完成线程的阻塞。
2. Monitor模型
Monitor实现对临界资源的保护,保证每次只有一个线程能进入代码块进行访问。进入代码块即为持有Monitor,退出代码块即为释放Monitor。
2.1 为什么sync是非公平锁
而未抢占到锁的资源,便会进入Monitor的Entry Set阻塞。当抢占到锁的方法遇到wait()方法后,会释放Monitor资源,并进入到Wait Set阻塞。当Monitor资源被释放后。Entry Set、Wait Set和刚进入Monitor的线程共同争夺Monitor资源。这就是sync是非公平锁的原因。
2.2 Monitor和对象头的关系
sync
是借助obj的monitor
对象实现当前线程的阻塞。代码块加锁是在前后分别加上monitorentry
和monitorexit
指令来实现的。obj被加锁后,可以在其对象头的mark word
的标记位体现。
2.3 Monitor的JDK源码
JDK源码如下图所示,可以看到Wait Set、Entry Set、count(重入次数)、owner(持有锁的线程)等属性。
ObjectMonitor() {
_header = NULL;
//获取管程锁的次数
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
//持有该ObjectMonitor线程的指针
_owner = NULL;
//管程的条件变量的资源等待队列
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
//管程的入口线程队列
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
3. sync各种锁与Monitor的关系
只有重量级锁才会借助Monitor,而偏向锁和轻量级锁均借助的是对象头中
mark word
的标识来实现的。
偏向锁和轻量级锁的目的:偏向锁、轻量级锁并不是来取代重量级锁的。而是在不同的场景下的相互补充。偏向锁和轻量级锁解决的是当临界资源没有被争夺访问的场景下,如何优化性能。
偏向锁和轻量级锁的实现:通过对象头mark word
的标记位,通过CAS来实现访问。无需借助Monitor。
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀。
偏向锁
只有一个线程访问sync保护的临界资源。而偏向锁的目的是某个线程获取锁后,消除这个线程重入的开销。偏向锁只需要在置换ThreadId的时候依赖一次CAS。
当两个线程交替访问(即线程A释放锁,线程B便去申请锁)时,线程B使用CAS是可以获取锁的。此时对象头的Markword中线程ID设置的是线程B的线程ID
轻量级锁
两个线程竞争同一把锁,线程B竞争锁失败,表明锁对象存在竞争,则会先撤销偏向锁模式,进入轻量级锁。刚开始CAS竞争轻量级锁失败时,不会立刻膨胀为重量级锁,而是采用自旋的方式,不断重试,尝试抢锁。JDK6中,默认开启自旋,自旋10次,JDK6引入自适应的自旋锁,对于只能指定固定次数的自旋进行优化,重试机制更加智能。
重量级锁
只有通过自旋依然获取不到锁的情况下,表明锁竞争比较激烈,不再适合额外的CAS操作消耗CPU资源。则直接膨胀为重量级锁。在此状态下,所有等待锁的线程必须进入阻塞状态。
借助obj的Monitor对象完成线程的互斥访问。而线程的阻塞借助操作系统完成。这个过程是比较耗费事件的。
在JDK1.6之后,sync关键字进行了优化,使用了自旋锁,让并发的线程先不借助monitor进行同步操作。而是自旋一段时间,等待临界区线程执行完毕。
3.1 锁膨胀过程
对象头中的MarkWord用于存储对象本身的运行时数据,记录了对象的Hash、锁、GC标记等相关信息。当使用synchronized
关键字加锁时,围绕同步锁的一系列过程均和MarkWord相关。
锁升级过程,可以总结为:无锁->偏向锁->轻量级锁(自旋锁,自适应锁)->重量级锁。且只能正向锁膨胀,不存在降级。
- 对象初始化,处于无锁状态;
- 存在线程A来获取锁,锁对象第一次被获取使用,进入偏向锁模式,且可重入。若另外一个线程B来获取锁,偏向锁可以被线程B的CAS获取到,那么替换markword中线程ID的相关信息即可。
- 当线程BCAS获取不到锁,则会升级为轻量级锁。在升级过程中也采用了CAS操作。若首次CAS获取或者竞争轻量级锁失败,则会采用spin(快速旋转)自旋的方式,旋转N次,重复尝试。JDK1.6默认开启自旋锁,自旋的次数默认是10次,也采用自适应式自旋的方式;
- 若经过自旋,依旧无法获取到锁,表明锁竞争比较激烈,CAS自旋较为消耗CPU资源,直接膨胀为重量级锁。
3.2 自适应式自旋
JDK1.6中引入了自适应的自旋锁。 自适应意味着自旋的时间不再是固定的, 而是由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。如果在同一个锁对象上, 自旋等待刚好成功获得锁, 并且在持有锁的线程在运行中, 那么虚拟机就会认为这次自旋也是很有可能获得锁, 进而它将允许自旋等待相对更长的时间。
3.3 锁消除
锁消除是一种更为彻底的优化,在JIT编译时,对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。
3.4 锁粗化
原则上,我们知道在加同步锁的时候,尽可能的将同步块作用范围限制在尽量小的范围。但是如果存在一连串的操作都是对同一个对象进行反复的加锁和解锁,甚至加锁的操作出现在循环体中,那么即使没有线程竞争共享资源,频繁的进行加锁操作也会导致性能的损耗。
锁粗化就是将加锁的范围粗化到这一连串的操作的外部(比如while循环体外)。使得这一连串操作只需要加一次锁即可。
4. wait和notify方法的疑问
有没有疑问:为什么wait和notify是Object的方法,而不是Thread的方法。且wait和notify方法必须在sync关键字中使用?
4.1 wait和notify为什么必须synchronized中使用
只因为synchronized关键字(重量级锁,开启了管程),使得对象指向了ObjectMonitor对象,所以调用对象的wait()和notify等方法才会将线程阻塞(加入到_WaitSet中)。
4.2 wait和notify为什么是Object的方法
又因为wait()和notify()是ObjectMonitor的方法。而Object对象头中保存了ObjectMonitor的指针,所以是Object便可操作wait()方法。
推荐阅读
Java并发基石——所谓“阻塞”:Object Monitor和AQS(1)
Java精通并发-通过openjdk源码分析ObjectMonitor底层实现
synchronized 锁优化(一):自适应自旋锁、锁消除、锁粗化
相关阅读
JAVA并发(1)—java对象布局
JAVA并发(2)—sync关键字和Monitor管程的关系
JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
JAVA并发(5)— 多线程顺序的打印出A,B,C(线程间的协作)
JAVA并发(6)— AQS源码解析(独占锁-加锁过程)
JAVA并发(7)—AQS源码解析(独占锁-解锁过程)
JAVA并发(8)—AQS公平锁为什么会比非公平锁效率低(源码分析)
JAVA并发(9)— 共享锁的获取与释放
JAVA并发(10)—interrupt唤醒挂起线程
JAVA并发(11)—AQS源码Condition阻塞和唤醒
JAVA并发(12)— Lock实现生产者消费者