一、synchronized应用场景
1.修饰实例的方法
public synchronzied void test() {
}
2.修饰静态方法
public static synchronized void test() {
}
3.修饰代码块
public void test() {
// 对当前对象this加锁
synchronized(this) {
...
}
// 对class对象加锁
synchronized(Test.class) {
...
}
}
二、对象头(Mark Word)
众所周知java锁是锁的对象,那么对象头上都存放了哪些信息呢,以32位jvm虚拟机为例
具体如何使用后面会详解
三、Lock record:
LockRecord用于synchronized为轻量级锁时的优化
锁是线程私有的,所以在java虚拟机栈中也会生成一条锁记录,Lock record就是栈中的锁记录,其中owner字段就是指向堆中被锁住的对象
jvm是如何使用的后面会详解
四、synchronized锁状态简介
synchronized共有四种锁的状态:分别为无锁、偏向锁、轻量级锁、重量级锁
1.无锁
当jvm不开启偏向锁时,synchronized默认为无锁状态,此时对象上markword的偏向模式为0,标志位为01
2.偏向锁
当jvm开启偏向锁时,synchronized虽然也是无锁但是为匿名偏向锁状态,此时对象上markword的偏向模式为1,标志位为01,当第一个线程获取到锁那么synchronized就升级为偏向锁
3.轻量级锁
当存在竞争时(超过1个线程抢占锁)绝大多数情况下synchronized会升级为轻量级锁,此时对象上的markword的偏向模式为null,标志位为00
4.重量级锁
当竞争非常激烈达到配置的阈值或者触发了适应性自旋算法(后面会讲),synchronized会膨胀为重量级锁,此时markword的偏向模式为null,标志位为10
五、锁升级流程
下图为在网上找到的一张锁升级的整体流程图
1.判断是否开启偏向锁
检测锁对象的MarkWord是否为可偏向状态,即是否为偏向锁标识1,锁标识位为01;
2.栈帧中插入Lock Record
如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间为之后操作做准备,Lock Record结构如上图
3.判断线程id是否为重入
接下来,则判断markword上的线程ID是否为当前线程ID,如果是,表示线程A已经获得了这个偏向锁,虚拟机不再进行任何同步操作,直接执行同步代码,不需要进行CAS操作来加锁和解锁,提高性能,所以synchronized从是否重入的维度是可重入锁,
4.CAS替换线程id
如果线程ID不为当前线程ID时那么将会使用CAS操作对象的Mark Word的线程id,用新的线程id替换原来的0,如果成功那么获取到偏向锁继续执行同步代码块,如果失败了就说明至少有一个线程已经获取到偏向锁了需要升级为轻量级锁,但是因为锁升级是不可逆的,所以此时虚拟机会做挽回策略避免不必要的升级
(1)首先会先等持有偏向锁的线程进入安全点(与gc时的安全点是一样的)暂停线程
(2)虚拟机会检查持有线程锁的线程状态是否存活,如果存活遍历栈帧中的Lock Record,判断该线程是否在执行同步代码块中的代码,如果是那就没办法只能升级
(3)如果线程状态不是存活的或者不在执行同步代码块中的代码将会判断是否开启重偏向。如果成功执行了重偏向那么将会重新设置为偏向锁状态,将线程id更新为新的线程id
重偏向:如果有很多对象,这些对象同属于一个类(假设是类A)被线程1访问并加偏向锁,线程1结束之后线程2来访问这些对象(不考虑竞争情况),通过CAS操作把这些锁升级为轻量锁,会是一个很耗时的操作。
JVM对此作了优化:
当对象数量超过某个阈值时(默认20, jvm启动时加参数-XX:+PrintFlagsFinal可以打印这个阈值 ),Java会对超过的对象作批量重偏向线程2,此时前20个对象是轻量锁,
后面的对象都是偏向锁,且偏向线程2。
因此如果不考虑重偏向的话,只要存在两个线程访问同一个对象那么 就会升级为轻量级锁
4.升级为轻量级锁
升级到轻量级锁后会先唤醒线程然后使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表表该线程拥有了这个对象的锁,synchronized也就升级为轻量级锁,并且对象Mark Word的锁标志位变为“00”
5.自旋优化
如果失败就说明至少存在一条线程与当前线程竞争获取该对象的锁。,虚拟机为了避免线程真实地在操作系统层面挂起,会进行自旋锁的优化,在经过若干次循环后,如果得到锁,就顺利执行同步代码。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。最后没办法也就只能升级为重量级锁了。
6.升级为重量级锁
在jvm规范中,synchronized是基于监视器锁(monitor)来实现的。会在同步代码之前添加一个monitorenter
指令,获取到该对象的monitor,同时它会在同步代码结束处和异常处添加一个monitorexit
指令去释放该对象的monitor,需要注意的是每一个对象都有一个monitor与之配对,当一个monitor被获取之后 也就是被monitorenter
,它会处于一个锁定状态,其他尝试获取该对象的monitor的线程会获取失败,只有当获取该对象的monitor的线程执行了monitorexit
指令后,其他线程才有可能获取该对象的monitor成功。
所以从上面描述可以得出,监视器锁就是monitor
它是互斥的(mutex)。由于它是互斥的,那么它的操作成本就非常的高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”
六、批量撤销机制
前面说到了批量重偏向就是如果一个类的大量对象被一个线程T1执行了同步操作,也就是大量对象先偏向了T1,T1同步结束后,另一个线程也将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作超过阈值20的会重新设置为偏向锁。
那批量撤销就是对重偏向的一个补充,当一个偏向锁如果撤销次数到达阈值40的时候就认为这个对象设计的有问题;那么JVM会把这个对象所对应的类所有的对象都撤销偏向锁;并且新实例化的对象也是不可偏向的。