概念
JDK提供的同步的关键字,通过synchronized可以锁定一个代码块或者一个方法,从而实现锁的效果。通过synchronized锁定的代码块或者方法同一时间只能由一个线程去执行,等这个线程执行完了释放锁了之后别的线程才能获取锁进入并且执行。
用法
注意:synchronized是对一个对象上锁的,使用的时候需要注意在对同一个对象上锁才能达到互斥的目的
Java实例对象的组成
- 对象头:MarkWord和一个指向一个类Class对象的指针;如果实例是数组,那么还会多一个数组长度
- 实例数据:实例的一些属性信息,基本类型存储值,引用类型存内存地址;如果是数组那么存指向的对象的内存地址
- 对齐填充:补齐作用,如果JVM对象大小不是8的倍数,那么就填充补齐为8的倍数
MarkWord 说明(32位)
锁模式 | Mark Word 32个bit | ||||
25bit | 4bit | 1bit | 2bit | ||
23bit | 2bit | 是否是偏向锁 | 锁标识位 | ||
无锁 | 对象的hashcode(延迟生成首次调用hashcode) | GC年龄 | 0 | 01 | |
偏向锁 | 偏向的线程ID | Epoch代数 | GC年龄 | 1 | 01 |
轻量级锁 | 指向锁记录的指针(锁记录在哪个线程的栈空间内,代表其获得锁),锁记录也就是原mar kword的数据 | 00 | |||
重量级锁 | monitor监视器地址 | 10 | |||
GC模式 | 空 | 11 |
底层monitor实现重量级锁
定义
monitor叫做对象监视器、也叫作监视器锁,JVM规定了每一个java对象都有一个monitor对象与之对应,这monitor是JVM帮我们创建的,在底层使用C++实现的。其实monitor在底层也是某个类的对象,那个类就是ObjectMonitor。
对象、Mark Work、monitor监视器的联系图:
关键属性解析
加锁解锁原理分析
加锁解锁通过_count和_owner体现
自锁优化
为什么1.6进行自旋优化?之前版本获取锁失败就直接挂起,然后唤醒之后再竞争锁,线程上下文切换频繁有性能问题。
为什么自旋?如果线程挂起后再唤醒需要耗时3000ms,但是线程A获得锁之后很快如120ms就释放锁,线程B采用每隔50ms自旋一次获取锁,那么只需要自旋3次耗时150ms就可以获取到锁,没有上下文切换,性能更高。
为什么有自旋最大次数限制?自旋时候是CPU空转,不释放CPU的,无限制的自旋等待,就是浪费CPU啥事也不干。
wait和notify
必须是当线程获取锁之后,才能调用wait()方法,然后此时释放锁,将_count恢复为0,将_owner指向 null,然后将自己加入到waitset集合中,等待别人调用notify或者notifyAll将其中waitset的线程唤醒。如果没有获取锁直接调用wait()
会报异常IllegalMonitorStateException
。
notify就是从waitset中随机挑一个线程来唤醒,只唤醒一个。notifyAll这方法就是将waitset中所有等着的线程全部唤醒了。唤醒后进入_entrylist
中一起竞争锁。
注意:wait和notify方法之后在获取了锁之后才能调用的,所以才需要写在synchronized方法块的内部。
因为waitset集合是monitor对象的一个属性,所以调用之前必须要获取到monitor对象的操作权限,也就是获取到锁,notify要操作waitset也是一样。否则会报异常IllegalMonitorStateException
锁重入、锁消除、锁升级原理
- 偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
- 轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
- 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
锁重入
正在持有锁的线程支持再次获取锁,不会出现自己锁死自己的问题。A获取到锁后,再次获取锁,发现_owner
是自己,那么_count +1
就可以获取锁。释放锁同理,如果是_count >1
,释放一次扣减1,直到0。
锁消除
在不存在锁竞争的地方使用了synchronized,jvm会自动帮你优化掉。如lock对象是线程私有的,多个线程不会共享;像这种情况多线程之间没有竞争,就没必要使用锁了。
优化前:
public void xxx(){
Object lock = new Object();
synchronized(lock){
j++;
i++;
// 其它业务
}
}
优化后:
public void xxx(){
// 不用锁 不存在多线程并发问题
j++;
i++;
// 其它业务
}
锁粗化
一般情况下,为了提高性能,总是将同步块的作用范围限制到最小,这样可以使得需要同步的操作尽可能地少。但如果一系列连续的操作一直对某个对象反复加锁和解锁,频繁地进行互斥同步操作也会引起不必要的性能消耗。如果虚拟机检测到有一系列操作都是对某个对象反复加锁和解锁,会将加锁同步的范围粗化到整个操作序列的外部。
// 粗化前
Object lock = new Object()
for(int i =0 ; i<0){
// 反复加锁解锁
synchronized(lock){
// 执行业务
}
}
// 粗化后
Object lock = new Object()
// 无需反复加锁解锁
synchronized(lock){
for(int i =0 ; i<0){
// 执行业务
}
}
锁升级
synchronized
如何从无锁到偏向锁到轻量级锁到重量级锁。锁升级的目的是花费最小的代价达到加锁的目的。
锁模式 | Mark Word 32个bit | ||||
25bit | 4bit | 1bit | 2bit | ||
23bit | 2bit | 是否是偏向锁 | 锁标识位 | ||
无锁 | 对象的hashcode(延迟生成首次调用hashcode) | GC年龄 | 0 | 01 | |
偏向锁 | 偏向的线程ID | Epoch代数 | GC年龄 | 1 | 01 |
轻量级锁 | 指向锁记录的指针(锁记录在哪个线程的栈空间内,代表其获得锁),锁记录也就是原mar kword的数据 | 00 | |||
重量级锁 | monitor监视器地址 | 10 | |||
GC模式 | 空 | 11 |
偏向锁
- 有线程第一进入
synchronized
代码块,发现mw
的后3位是001
表示无锁且允许偏向,选择代价最小的方式,加了个偏向锁,只在第一次获取偏向锁的时候执行CAS操作(将自己的线程Id通过CAS操作设置到Mark Word中),同时将偏向锁标志位改为1。 - 后面如果自己再获取锁的时候,每次检查一下发现自己之前加了偏向锁,就直接执行代码,就不需要再次加锁了。
- 释放完锁之后,不能将
mw
改回去是,所有偏向锁无法恢复为无锁。 - 疑问是:hashcode是否是丢失了呢?什么时候不影响偏向呢?看后面的
hashcode()
引起的锁膨胀。
这个哥们不释放锁,如果它用完了,别人这个时候需要进入synchronized代码块怎么办?
偏向锁的获取流程
引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。
偏向锁的撤销
只有等到竞争,持有偏向锁的线程才会撤销偏向锁。偏向锁撤销后会恢复到无锁或者轻量级锁的状态。
- 偏向锁的撤销需要到达全局安全点,全局安全点表示一种状态,该状态下所有线程都处于暂停状态。
- 判断锁对象是否处于无锁状态,即获得偏向锁的线程如果已经退出了临界区,表示同步代码已经执行完了。重新竞争锁的线程会进行CAS操作替代原来线程的ThreadID。
- 如果获得偏向锁的线程还处于临界区之内,表示同步代码还未执行完,将获得偏向锁的线程升级为轻量级锁。
偏向锁之重偏向
- 线程B去申请加锁,发现是线程A加了偏向锁;这时候回去判断一下线程A是否存活,如果线程A挂了,就可以重新偏向了,重偏向也就是将自己的线程ID设置到Mark Word中。
- 如果线程A没挂,但是synchronized代码块执行完了,这个时候也可以重新偏向了,将偏向标识指向自己
如果线程B在申请获取锁的时候,线程A这哥们还没执行完synchronized同步代码块怎么办?这个时候就有锁的竞争了,这就需要将锁升级一下了,线程B就会把锁升级为轻量级锁。
轻量级锁
轻量级锁模式下,加锁之前会创建一个锁记录,然后将Mark Word中的数据备份到锁记录中(Mark Word存储hashcode、GC年龄等很重要数据,不能丢失了),以便后续恢复Mark Word使用。
这个锁记录放在加锁线程的虚拟机栈中,加锁的过程就是将Mark Word 前面的30位指向锁记录地址。所以mark word的这个地址指向哪个线程的虚拟机栈中,就说明哪个线程获取了轻量级锁。
偏向锁升级为轻量级锁
轻量级锁模式下锁竞争和释放
引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
- 线程A和线程B同时竞争锁,在轻量级锁模式下,都会创建Lock Record锁记录放入自己的栈帧中
- 同时执行CAS操作,将Mark Word前30位设置为自己锁记录的地址,谁设置成功了,锁就获取到锁
- 轻量级锁的释放将自己的Lock Record中的Mark Word备份的数据恢复回去即可,恢复的时候执行的是CAS操作将Mark Word数据恢复成加锁前的样子。
- 解锁失败,说明有其它线程请求过该锁,锁已经升级为重量级锁了。
重量级锁
重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。斥锁(重量级锁)也称为阻塞同步、悲观锁。
有哪几种方式可以使一把锁升级为重量级状态?
调用wait方法
在同步代码块中调用对象的hashcode方法
重量级锁的自旋优化
线程沉睡后再唤醒的成本比较大!
获取锁失败之后的线程自己先原地等一段时间,然后再去重试获取锁,这种方式就叫做自旋。
monitor有一个_spinFreq参数表示最大自旋的次数,_spinClock参数表示自旋的间隔时间。所以自旋最多会重试_spinFreq次。
每次失败之后等_spinClock的时间过后再去重试,如果尝试_spinFreq次之后都没有成功,那没辙了,只能沉睡了。
为什么说重量级锁开销大呢
系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
锁升级总结
JVM设计的这套synchronized锁升级的原则,主要是为了花费最小的代价能达到加锁的目的。
- 在没有竞争的情况下,进入synchronized的使用使用偏向锁就够了,这样只需要第一次执行CAS操作获取锁,获取了偏向锁之后,后面每次进入synchronized同步代码块就不需要再次加锁了。然后在存在多个线程竞争锁的时候就不能使用偏向锁了,不能只偏心一个人,它优先获取锁,别人都看它表演,这样是不行的。
- 于是就升级为轻量级锁,在轻量级锁模式在每次加锁和释放是都需要执行CAS操作,对比偏向锁来说性能低一点的,但是总体还是比较轻量级的。
- 重量级锁下,为了尽量提升线程获取锁的机会,避免线程陷入获取锁失败就立即沉睡的局面(线程沉睡再唤醒涉及上下文切换,用户态内核态切换,是一个非常重的操作,很费时间),所以设计自旋等待;线程每次自旋一段时间之后再去重试获取锁
- 当竞争非常激烈,并发很高,或者是synchronized代码块执行耗时比较长,就会积压大量的线程都在自旋,由于自旋是空耗费CPU资源的,也就是CPU在那等着,做不了其他事情,所以在尝试了最大的自旋次数之后;及时释放CPU资源,将线程挂起了。
synchronized偏向锁后hashcode存在哪里
结论:
- jdk8偏向锁是默认开启,但是是有延时的,可通过参数: -XX:BiasedLockingStartupDelay=0关闭延时。
- hashcode是懒加载,在调用hashCode方法后才会保存在对象头中
- 当对象头中没有hashcode时,对象头锁的状态是 可偏向( biasable,101,且无线程id),也叫“匿名偏向锁”
- 如果在同步代码块之前调用hashCode方法,则对象头中会有hashcode,且锁状态是 不可偏向(0 01),这时候再执行同步代码块,锁直接是 轻量级锁(thin lock,00)
- 如果获得了偏向锁,然后在同步代码块中执行hashcode,则锁是从 偏向锁 直接膨胀为 重量级锁
1、hashcode是啥时候存进对象头中?调用hashcode()
2、存在hashcode后,出现synchronized会是什么锁?轻量级
3、如果锁状态是 已偏向,再计算hashcode会怎样?膨胀为重量级
锁升级发生后,hashcode去哪了?
- 在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。
- 对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法己经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的话,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
- 升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
- 升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。
手动验证思考
- 偏向锁中,调用await,会膨胀为重量级锁,是否调用重写的hashCode()? 没有调用