【并发编程】(二)Java并发机制底层实现原理——synchronized关键字

目录

  • synchronized定义
  • synchronized实现原理
    • 对象头
    • 锁分类
      • 1. 偏向锁
      • 2. 轻量级锁
      • 3. 重量级锁
    • 锁升级过程

synchronized定义

synchronized
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。

  • 在【并发编程】(一)Java并发机制的底层实现原理——volatile关键字中,介绍了JMM:java内存模型。

JMM中最重要的问题:重排序、原子性、可见性。而volatile、synchronized、final、和Lock都可以保证可见性:

  • volatile:通过禁止违背Happends-before原则的重排序操作,和变量改变时立即刷新值到主存,来保证。
  • synchronized和lock:都是通过把并发的过程强制转化为原子化的过程;
  • final:变量本来就是放在多个线程共享的区域,自然可见;

synchronized实现原理

对象头

在HotSpot虚拟机中,java对象在虚拟机中的存储布局分为3块区域:对象头、实例数据和对齐填充;而synchronized用的锁就是存在java对象头里的。

  • java对象头:
长度 内容 说明
32/64bit Mark Word 存储对象的hashcode或锁信息
32/64bit Class Metadata Address 存储对象类型数据的指针
32/64bit Array Length 数组的长度(如果当前对象是数组)
  • 32位的JVM的Mark Word中的默认存储结构无锁状态如下:
锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象的hashcode 对象分代年龄 0 01
  • 在运行期间,Mark word中存储的数据会随着锁标志位的变化而变化:
锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
偏向锁 线程ID I Epoch 对象分代年龄 1 01
锁状态 25bit I 4bit I 1bit是否是偏向锁 2bit锁标志位
轻量级锁 指向栈中锁记录的指针 00
锁状态 25bit I 4bit I 1bit是否是偏向锁 2bit锁标志位
重量级锁 指向重量级锁的指针 10

锁分类

Java SE 1.6为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。在Java SE 1.6中锁一共有4种状态:

  • 无锁 ——> 偏向锁 ——> 轻量级锁 ——> 重量级锁

这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级。

1. 偏向锁

HotSpot的作者发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得;当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程id。

以后该线程在进入和退出同步块时不需要进行CAS来加锁和解锁;只需要简单地测试一下对象头中是否存储着指向当前线程的偏向锁。

  • 偏向锁的撤销:当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放。
  • 偏向锁的优点:当只有一个线程执行同步块时,不需要CAS操作直接获取锁,提升性能;
  • 适用于一个线程反复获取同一个锁的情况。

2. 轻量级锁

轻量级锁,也叫自旋锁:
线程在执行同步块之前,JVM会在当前的 栈帧 中创建Lock Record用于存储锁记录的空间,将对象头中的Mark Word复制到Lock Record,官方称为Displaced Mark Word。

线程尝试使用CAS将对象头中的Mark Word替换为 指向锁记录的指针

  • 如果成功,当前线程获取锁;
  • 如果失败,表示其他线程竞争锁,当前线程尝试使用 自旋 来获取锁。

轻量锁的解锁:使用原子的CAS将Displaced Mark Word替换回到对象头:

  • 如果成功,表示没有竞争发生;
  • 如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁。

3. 重量级锁

升级为重量级锁后,Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入到 阻塞 状态。
Synchronzed的重量级锁是通过对象内部的一个监视器锁 Monitor 来实现的。而Monitor又是依赖于底层操作系统的Mutex Lock互斥锁实现的。而操作系统实现线程之间的切换需要从用户态转为核心态,成本很高。

  • Monitor:同一个时刻,只有一个进程或者线程可以进入monitor中定义的临界区,这使得monitor可以达到互斥的效果。而无法进入临界区的进程或线程,它们应该被阻塞,并且在必要的时候被唤醒。
  • Monitor需要的元素:
    • 临界区
    • monitor对象及锁
    • 条件变量及定义在monitor对象上的wait、signal操作

在Java语言中,我们知道使用synchronized关键字时,总是需要指定一个对象与之关联,比如this或者this.Class对象。这个对象就是 Monitor Object ,即java.lang.Object。Object对象提供了wait()、notify()等方法对monitor机制进行了封装。

  • Java Monitor原理:
    【并发编程】(二)Java并发机制底层实现原理——synchronized关键字_第1张图片
  1. 当线程需要获取一个对象的锁时,会被放入EntrySet进行等待;
  2. 此线程获取了锁,成为锁的拥有者,进入到The Owner区域;
  3. 拥有者线程可以调用wait()释放锁,进入WaitSet进入等待状态;
  4. 其他线程调用notify()唤醒等待集合中的线程

锁升级过程

  1. 当前没有多线程的竞争,只有一个线程去获取锁, 进入偏向锁模式 (在对象头中保存线程ID)
  2. 另一个线程来争取锁:判断对象是否为偏向状态(此时为是)、判断对象头中保存的线程id是否为本线程id(此时不是):那么尝试通过CAS设置为当前线程ID
    • 成功(因为上一个持有偏向锁的线程是不会主动释放的),获取偏向锁并执行代码块;
    • 失败(表示有线程竞争的情况存在),撤销偏向锁 模式,准备升级轻量级锁:
      • 在栈帧中开辟一片锁记录空间,用于存储当前对象Mark Word的拷贝;
      • 使用CAS操作尝试把对象的Mark Word更新为指向此线程的栈帧锁记录指针。
        CAS成功,表示成功获取锁:将 锁模式设置为轻量级锁
        CAS失败,采用 自旋 方式不断重试
  3. 自旋当超出一定次数时,表示竞争激烈, 直接升级为重量级锁 。在此状态下,所有等待的线程都进入到阻塞状态,而不再CAS重试。

你可能感兴趣的:(Java并发,java,多线程,并发编程)