synchronized的理解使用及锁的状态和升级

1. synchronized使用:

synchronized作为保证线程安全的常规手段之一,其可以修饰实例方法、静态方法和代码块
 修饰实例方法,锁住的是实例对象this
 修饰静态方法,锁住的是类对象
 修饰代码块,锁住的是括号内的对象

2. synchronized的原理
 2.1. Monitor

每一个对象都关联一个监视器锁(monitor),当monitor被占用时就会处于锁定状态。

在Java虚拟机(HotSpot)种,monitor是由ObjectMonitor实现的,内部主要有_count、_owner、_EntryList、_WaitSet属性
 _count:为线程进入加锁代码的次数
 _owner:为持有锁的线程,即持有ObjectMonitor对象的线程
 _EntryList:是想要持有锁的线程的集合(保存ObjectWaiter,每个等待锁的线程都会封装成ObjectWaiter对象)
 _WaitSet:是加锁对象调用wait()方法后,等待被唤醒的线程的集合

 【多线程访问同步代码】

 ·首先会进入_EntryList,当线程获取到对象(objectref)的monitor后,会把monitor的owner变量设置为当前线程,同事count变量加1
 ·若线程调用了wait()方法,将释放当前持有的monitor,owner变量回复为null,count自减1,同时该线程进入_WaitSet种等待唤醒
 ·当线程执行完毕,释放monitor(锁)并复位变量的值,以便其他的线程获取monitor

在这里插入图片描述

 2.2. 原理:

  2.2.1.【修饰代码块】:

public class SynchronizeTest {
  public void method(){
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "+++++++" + i);
            }
        }
    }
}

  【反编译class文件】:

在这里插入图片描述

synchronized修饰代码块,在进入代码块前,JVM会先执行monitorenter指令,试图获取objectref(锁对象)锁对的moniotr的所有权:
·
 如果目标锁对象的monitor的count为0,声明此对象没有被其他线程持有,此时把monitor中的owner变量设置为当前线程,同时count加1;
 如果monitor的count不为0,判断持有的线程是否是当前线程(owner是否指向当前线程):
  如果是,则计数器count再次加1(锁的可重入性)
  如果不是,声明monitor不是当前线程持有,则当前线程阻塞等待,直至monitor被释放
·
同步代码执行完后遇到monitorexit指令,执行指令后线程将释放monitor(锁)并设置计算器count为0,这样其他线程才有机会持有锁
可以看见,还有一个monitorexit指令,那是为了保证monitor的释放,也就是同步代码出现异常,也能保证锁的正常释放

  2.2.1.【修饰同步方法】:

public class SynchronizeTest {
   public synchronized void method() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "+++++++" + i);
        }
    }
}

  【反编译class文件】:

在这里插入图片描述

synchronized修饰同步方法,当方法调用时,将检查方法的ACC_SYNCHRONIZED访问标识是否被设置
·
 如果设置了,该线程就先持有monitor,然后执行同步方法,最后再方法完成(无论正常或异常结束)时释放monitor
·
所以,同步方法通过ACC_SYNCHRONIZED标识来获取锁

3. 锁的状态标识:

一个对象由三部分组成:对象头,实例数据、数据填充;锁状态标识储存再对象头的Mark Word里

在这里插入图片描述
【Mark Word再对象头的布局】:

 在32为的虚拟机中:
在这里插入图片描述

 在64为的虚拟机中:
在这里插入图片描述
 可见,不同的锁状态由对应的锁标识:
在这里插入图片描述

4. 锁的状态及锁的升级:

 Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,对synchronized锁进行了优化
 Java6之后,锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,随着竞争J情况逐渐升级

 4.1 【偏向锁】:

偏向锁是Java 6 新添的内容,并且jdk默认启动的选项,可以通过-XX:-UseBiasedLocking 来关闭偏向锁。
另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。

  【加锁过程】:

如果JVM支持偏向锁,那么在分配对象时,分配一个可偏向而未偏向的对象(Mark Word的后三位为101,并且线程ID字段初始为0)
如果JVM关闭偏向锁,那么在分配对象时,分配一个无锁不可偏向的对象(Mark Word的后三位为001,前25bit为hashcode值)
可见,无锁和偏向锁是互斥的,也就是不能从无锁升级到偏向锁

在这里插入图片描述

【线程访问同步代码时】:先检查Mark Word是否为可偏向状态,即是否为偏向锁为1,锁标识位位01;
·
 若为可偏向状态,则检查Mark Word的线程ID是否为当前线程ID
  若不是,将通过CAS来尝试将对象头的Thread ID设置为当前线程ID
   如果设置成功,则获得锁,那么线程再次进入和退出同步代码块时,就不需要CAS来获得锁,只是检查Mark Word的线程是否指向当前线程
   如果设置失败,声明存在锁的竞争,那么将执行偏向锁的撤销操作,将偏向锁升级为轻量级锁
  如果为当前线程ID,则直接执行同步代码
·
·【无锁➡轻量级锁】
·
 若不是可偏向状态,即对象为无锁不可偏向的对象,则JVM首先将在当前线程的栈帧中建立一个锁记录(Lock Record)空间,用于储存锁对象目前Mark Word的拷贝(官方称之为Displaced Mark Word);
 然后线程尝试CAS将对象头中的Mark Word替换为指向锁记录的指针
  如果替换成功,表示竞争到锁,则将锁标识位变为00(表示处于轻量级锁状态),然后执行同步代码
  如果替换失败,则判断当前对象的Mark Word是否指向当前线程栈帧的所记录
   如果指向,则说明当前线程已经拥有这个对象锁,则可以直接指向同步代码
   如果不指向,则说明锁被其他线程竞争持有,当前线程便尝试通过自选来获取锁。当线程的自选次数达到阈值,轻量级锁膨胀为重量级锁

在这里插入图片描述

 4.2 【偏向锁➡轻量级锁】:

上文提到,如果CAS失败,则会进行偏向锁的撤销操作
偏向锁的撤销操作需要在全局检查点(global safepoint)执行,在全局检查点上没有线程执行字节码。
【撤销过程】:
 通过Mark Word 中已存在的Thread ID找到成功获取偏向锁的那个线程
·
  如果该线程拥有锁,则将偏向锁升级为轻量级锁:
    在该线程的栈帧中补充锁记录(Lock Record),然后将被获取了偏向锁对象的Mark Word更新为指向这个所记录的指针
·
  如果该线程不拥有锁(同步代码执行完,锁释放了)
    如果允许重偏向,那么将Mark Word的线程ID重置为0(还是偏向锁)
    如果不允许重偏向,那么将Mark Word设置为无锁状态,即后两位为01(无锁不可偏向状态)

 4.3 【轻量级锁➡重量级锁】:

上文提到,线程尝试CAS将对象头中的Mark Word替换为指向锁记录的指针,如果失败,便尝试自选来获取锁
当竞争线程的自旋次数达到阈值,轻量级锁就会膨胀为重量级锁

 4.4 【轻量级锁的解锁】:

轻量级锁几所时,如果对象的Mark Word仍指向线程的锁记录,会使用CAS操作,将Dispalced Mark Word替换到对象头
如果成功,则表示没有竞争发生,释放成功
如果失败,表示当前锁存在竞争,锁会膨胀为重量级锁
·
【疑问】:什么时候会对轻量级锁解锁???我也不知道

 4.5 【重量级锁】:

方正我也看不明白,java6之前synchronized是重量级的锁操作

 4.6 【关于无锁时的hashcode】:

在无锁状态下,Mark Word中可以存储对象的identity hash code值
当对象的hashCode()方法,并将该值存储到Mark Word中
后续如果该对象的hashCode()方法再次被调用则不会再通过JVM进行计算得到,而是直接从Mark Word中获取
只有这样才能保证多次获取到的identity hash code的值是相同的
·
在HotSpot, 调用hashCode(), 或者System.identityHashCode() 方法会导致对象撤销偏向锁

 【疑问】:

 不是引入了偏向锁么,为什么synchronized反编译后还会有monitorenter指令,这个不是获取重量级锁monitor的么?
 那么执行代码块之前是走monitorenter的流程还是偏向锁的流程?求dalao解答。

【参考】:
  深入理解Java并发之synchronized实现原理
  三面阿里,最后问了Synchronized底层原理
  JVM锁简介:偏向锁、轻量级锁和重量级锁
  深入分析synchronized原理和锁膨胀过程(二)
  深入分析synchronized的实现原理
  偏向锁与hashcode能共存吗?
  Java中的偏向锁,轻量级锁, 重量级锁解析

在这里插入图片描述

以上为个人理解,不对之处欢迎指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值