一、偏向锁

偏向锁是对轻量级锁的优化,正常轻量级锁是使得obj的Mark Word指向lock record的地址,且当轻量级锁没有竞争的时,每次在同一线程内的重入仍然需要执行CAS操作(即同一个线程内的synchronized嵌套synchronized)

Java6 中引入偏向锁来进一步优化:只有第一次使用CAS将线程ID设置为到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

相当于将线程的名字刻在对象中,这也是“偏向”的含义,即给对象所加的锁偏向于给某一线程使用。

例如:

  1. static final Object obj = new Object();
  2. public static void m1(){
  3. synchronized(obj){
  4. //同步块A
  5. m2();
  6. }
  7. }
  8. public static void m2(){
  9. synchronized(obj){
  10. //同步块B
  11. m3();
  12. }
  13. }
  14. public static void m3(){
  15. synchronized(obj){
  16. //同步块C
  17. }
  18. }
  • 不使用偏向锁的情况

image.png

  • 使用偏向锁的情况

image.png

第一次synchronized的时候将obj的Mark Word字段置为ThreadID,当线程内第二次synchronized的时候直接检查Mark Word中的Thread ID是否是当前线程,如果是代表当前线程已经持有锁,即不用再执行CAS操作,节省时间。

二、偏向状态

image.png

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword的值为0x05即最后3位为101,这时它的thread、epoch、age都为0
  • 偏向锁默认是延迟的,不会在程序启动时默认生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。

注意:

  • 处于偏向锁的对象解锁后,线程id仍存储于对象头中,所以偏向锁适用于单线程没有竞争的情况,偏向锁无法适用于多线程竞争锁的情况。
  • 如果偏向锁状态的对象调用hashcode()方法后,偏向锁直接被禁用。因为偏向锁实际上是一种非常简单的锁,只处理单线程,且其markword字段记录的是thread等信息。不像轻量级锁和重量级锁都有记录原对象hashcode等信息的位置,偏向锁如果想要记录hashcode值,只能从偏向锁转为无锁normal状态来记录hashcode值。
  • 注意hashcode只有在调用hashcode()方法之后才会有,否则初始值默认

可以添加VM参数:-xx:-UseBiasedLocking禁用偏向锁,那么发现synchronized加锁后,加的是轻量级锁。
所以综上,整个synchronized语法不变,但优先级的顺序是:偏向锁(默认)->轻量级锁->重量级锁

三、撤销偏向锁

撤销-调用对象hashCode

调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁被撤销

  • 轻量级锁会在锁记录中记录hashCode
  • 重量级锁会在Monitor中记录hashCode

在调用hashCode后使用偏向锁,记得去掉:-xx:-UseBiasedLocking

输出:
image.png

撤销-其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
具体流程为:线程2来执行synchronized会尝试为obj加偏向锁,但发现obj的Thread ID已经为线程1的ID值,即obj对象已经偏向线程1,这时会将偏向锁升级为轻量级锁,进入轻量级锁执行流程,当线程2退出synchronized开始解锁时,进入轻量级锁解锁流程,对象会被重新置为无锁状态。

注意升级成轻量级锁的条件是:多个线程之间没有竞争,时间上错开,这也是轻量级锁的定义。

四、批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次后,jvm会认为偏向错误,于是会在给这些对象加锁时重新偏向至加锁线程

具体代码思路为:

在线程1中for循环30次,每次给对象一个synchronized,即加上偏向锁。
在线程2中也同样for循环30次,也每次给对象一个synchronized,正常来说线程2执行synchronized后会释放轻量级锁变为无锁状态,即撤销偏向锁,但是jvm认为线程2执行这么多次撤销操作,可以认为对象可以偏向线程2,故后续不再撤销偏向锁,而是将偏向锁加在线程2上。

五、批量重偏向

当撤销偏向锁阈值超过40次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。(给jvm整emo了)

六、锁消除

加锁会对性能产生损耗,锁消除则是java自动对锁优化,例子如下:

  1. public class SuoXiaoChu {
  2. static int x=0;
  3. public void a() throws Exception{
  4. x++;
  5. }
  6. public void b() throws Exception{
  7. Object o =new Object();
  8. synchronized (o){
  9. x++;
  10. }
  11. }
  12. }

对象o没有被多线程共享,所以java会自动将synchronized()代码块去掉,所以实际显示a()方法和b()方法耗时差不多。