Synchronized锁原理和锁升级过程

Synchronized原理

1.Synchronized修饰范围

  • 修饰静态方法

此时锁的是位于元空间的Class字节码文件,也叫Class模板

  • 修饰实例方法

此时锁的是当前实例对象,相当于Synchronized(this),对同一个对象实例的范围进行锁,不同对象没有牵连

通过反编译,JVM执行时方法其实是被标识为ACC_SYNCHRONIZED,表明这是一个同步方法,而没有通过 monitorenter和monitorexit指令

  • 修饰代码块

需要指定锁对象如this 或者class模板 或者自定义final static对象

反编译角度看上面三个方式

修饰静态方法或者实例方法时,都是通过ACC_SYNCHRONIZED关键字,表明这是一个同步方法,而没有通过 monitorenter和monitorexit指令 。通过 ACC_STATIC 判断是静态方法还是实例方法

修饰代码块时,利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁

因为锁的那个对象中存放了正在锁它的线程信息,Synchronize也实现了可重入锁,即同一个线程自己反复获取锁可以直接拿到 而不会产生锁等待

Synchronized锁升级过程

无锁状态、偏向锁、轻量级锁、重量级锁 ,这是锁膨胀的过程,不可逆,但只有偏向锁可以变回无锁态。

解释名词

(1)无锁状态:对象还没有被任何线程锁定

(2)偏向锁: 经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁 。 因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,因此引入了偏向锁 。

这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假定没有其他线程来竞争,所以持有偏向锁的线程将永远不需要进行同步操作。让有其他线程来竞争时就会要么自己已经执行完毕锁偏向给了其他线程,要么竞争失败,锁会在一个安全点上撤销,并升级委轻量级锁。

(3)轻量级锁: 轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

(4)重量级锁: 依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁 ,只有当前线程可以工作,其他需要获取该锁的线程都会进入阻塞状态。

锁的详细升级过程

1.一开始对象是无锁状态的

2.一个线程尝试执行Synchronize代码块时,成功获得对象的锁,通过CAS操作往该对象markword中插入当前线程id, 同时修改偏向锁的标志位 。此时是偏向锁(偏向这个线程的锁,锁计数+1),同一个线程可以重复进入该锁,锁计数+1,执行完毕会锁计数-1,直到锁计数复0,释放锁。

正是因为有记录线程id,所以Synchronized实现了可重入锁的逻辑(简单说就是一个锁的拥有者可以重复的获取自己的锁,而不会产生阻塞问题)

关于扫描时markword,可以去了解下对象在JVM中的结构,这里简单说明,每个对象都会有一个对象头markword,这块区域可以存放hashcode和锁信息以及GC信息

32位JVM

64位JVM

3.当其他线程来获取锁时,发现markword中已经有线程id不是自己,通过一系列判断,如果竞争到锁,自己就得到偏向锁,通过CAS写入自己线程id。否则失败,此时偏向锁会找一个安全点撤销并升级为轻量级锁。

5.当轻量级锁已经有一个线程在进行自旋时,又有新线程来获取锁,或者线程自选达到限定次数(默认10次)。轻量级锁就会膨胀为重量级锁

6.重量级锁,会导致所有竞争的线程都进入阻塞状态。

总结与对比

当场景一般是单线程再执行时更适合偏向锁

当少量线程并且同步块执行时间偏短时,适合轻量级锁