synchronized(锁的升级 以及升级锁的场景 ,原理)

synchronized 的基本认识

在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着 Java SE 1.6 对
synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。这块在后续我们会慢慢展开

synchronized 的基本语法

synchronized 有三种方式来加锁,分别是

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前
    要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要
    获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同
    步代码库前要获得给定对象的锁。
    不同的修饰类型,代表锁的控制粒度

synchronized 的应用

/**
 * @Author jyl
 * @Date 2021/12/20 14:48
 * @Version 1.0
 * 数据安全的案例
 */
public class AtomicDemo {

    private static int count=0;//正确性

    public /*synchronized*/ static void incr(){
        synchronized (AtomicDemo.class) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<1000;i++){
            new Thread(()->AtomicDemo.incr()).start();
        }
        Thread.sleep(5000);
        System.out.println("运行结果:"+count);

    }

}

synchronized 锁的升级

了偏向锁、轻量级锁、重量级锁。在分析这几种锁的区别时,我们先来思考一个问题使用锁能够实现数据的安全性,但是会带来性能的下降。不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求。hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁
的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率,是的 synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。因此大家会发现在 synchronized 中,锁存在四种状态
分别是:无锁、偏向锁、轻量级锁、重量级锁; 其实偏向锁和轻量级锁也是无锁状态
锁的状态根据竞争激烈的程度从低到高不断升级。

线程的竞争机制
   synchronized (AtomicDemo.class) {
   //这里是临界区
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }

情况一:只有 Thread#1 会进入临界区;
情况二:Thread#1 和 Thread#2 交替进入临界区,竞争不激
烈;
情况三:Thread#1/Thread#2/Thread3… 同时进入临界区,
竞争激烈
以上这三种情况 分别对应 偏向锁 轻量级锁 和重量级锁

synchronized 偏向锁,轻量级锁,重量级锁的原理

对象在内存中的存储结构 和锁的存储

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
在这里插入图片描述

不同的锁存储这不同的状态 假设这里是32位的机器可以看 每个锁在对象头里面存贮的方式都不一样 可能看到这张图感觉还没有理解对象头到底是个啥
在这里插入图片描述
jvm源码图
instanceOop 实际上java虚拟机的一个 class 类对象 就是新建了一个 instanceOop的实例 可以看到红框里面他有个父类oopDesc
当我们在 Java 代码中,使用 new 创建一个对象实例的时候,(hotspot 虚拟机)JVM 层面实际上会创建一个instanceOopDesc 对象。
Hotspot 虚拟机采用 OOP-Klass 模型来描述 Java 对象实例,OOP(Ordinary Object Point)指的是普通对象指针,Klass 用来描述对象实例的具体类型。Hotspot 采用instanceOopDesc 和 arrayOopDesc 来 描述对象 头,arrayOopDesc 对象用来描述数组类型
instanceOopDesc 的定义在 Hotspot 源 码 中 的instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp
在这里插入图片描述
可以看到图片里面有个markOop _mark 这就是实际的对象头
在这里插入图片描述
点进去发现 这个对象头存贮了很多 枚举类 可以看到枚举类可以和上面蓝色的表格对应

在这里插入图片描述
注意:这里锁的对象头是锁的对象头不是当前调用的对象头

 synchronized (look) {  //这里存储锁信息的对象是 look
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }

偏向锁的原理

大部分情况下,锁不仅仅不存在多线程竞争,而是总是由同一个线程多次获得,为了让线程获取锁的代
价更低就引入了偏向锁的概念。怎么理解偏向锁呢?
当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步
锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等
表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了

偏向锁的获取和撤销逻辑
  1. 首先获取锁 对象的 Markword,判断是否处于可偏向状
    态。(biased_lock=1、且 ThreadId 为空)
  2. 如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID
    写入到 MarkWord
    a) 如果 cas 成功,那么 markword 就会变成这样。
    表示已经获得了锁对象的偏向锁,接着执行同步代码

    b) 如果 cas 失败,说明有其他线程已经获得了偏向锁,
    这种情况说明当前锁存在竞争,需要撤销已获得偏向
    锁的线程,并且把它持有的锁升级为轻量级锁(这个
    操作需要等到全局安全点,也就是没有线程在执行字
    节码)才能执行
  3. 如果是已偏向状态,需要检查 markword 中存储的
    ThreadID 是否等于当前线程的 ThreadID
    a) 如果相等,不需要再次获得锁,可直接执行同步代码

    b) 如果不相等,说明当前锁偏向于其他线程,需要撤销
    偏向锁并升级到轻量级锁
偏向锁的图解

偏向锁的图
在这里插入图片描述

偏向锁 MarkWord 图解
在这里插入图片描述

轻量级锁的基本原理

轻量级锁的加锁和解锁逻辑

锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:

  1. 线程在自己的栈桢中创建锁记录 LockRecord。
  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
  3. 将锁记录中的 Owner 指针指向锁对象。
  4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。

在这里插入图片描述
在这里插入图片描述
看完这两幅图可以理解为 所记录表是个对象变量 a =null 对象头是一个变量b =对象头信息 锁升级了 相当于 ab 变量值互换 但是不同的是 a 变量在互换后还存了 对象信息

自旋锁(轻量级锁)

轻量级锁的加锁

轻量级锁在加锁过程中,用到了自旋锁所谓自旋,就是指当有另外一个线程来竞争锁时,这个线
程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。所以通过看似无异议的循环反而能提升锁的性能。但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次,可以通过 preBlockSpin 来修改在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接
阻塞线程,避免浪费处理器资源

轻量级锁的解锁

轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的
MarkWord 中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

图解

在这里插入图片描述

重量级锁的基本原理

先执行反编译的指令
在这里插入图片描述

在这里插入图片描述
通过上面图我们看到了重量级锁其实就是一个监视器

在这里插入图片描述
加了同步代码块以后,在字节码中会看到一个
monitorenter 和 monitorexit。每一个 JAVA 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程
可以尝试去获得这个监视器monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

在这里插入图片描述
任意线程对 Object(Object 由 synchronized 保护)的访问,首先要获得 Object 的监视器。如果获取失败,线程进
入同步队列,线程状态变为 BLOCKED。当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻
塞在同步队列中的线程,使其重新尝试对监视器的获取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值