Java多线程学习入门(六):锁升级、锁消除、锁粗化

开始时间:20220915
课程链接:尚硅谷2022版JUC并发编程

锁升级

无锁->偏向锁->轻量级锁->重量级锁

Monitor 与java对象以及线程是如何关联?

  • 如果一个Java对象被某个线程锁住,则该Java对象的Mark Word字段中LockWord指向monitor的起始地址(复习一下对象的内存布局
  • Monitor的Owner字段会存放拥有相关联对象锁的线程id

在这里插入图片描述
Mutex Lock
Monitor是在JVM底层实现的,底层代码是C++。本质是依赖于底层操作系统的MutexLock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间成本非常高。所以synchronized是Java语言中的一个重量级操作。

在Mark Word里面会标记无锁态还是偏向锁还是轻量级锁、重量级锁,通过标志位来确定

  • 偏向锁:MarkWord存储的是偏向的线程ID;
  • 轻量锁: MarkWord存储的是指向线程栈中Lock Record的指针;
  • 重量锁:MarkWord存储的是指向堆中的monitor对象的指针;

无锁

初始状态,一个对象被实例化后,如果没有被任何线程竞争锁,就是无锁状态

偏向锁BiasedLock

Java15以后偏向锁被废除了。。
偏向锁:单线程竞争
当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。
如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
当一段同步代码一直被同一个线程多次访问,由于只有一个线程那该线程在后续访问时便会自动获得锁

  • 多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一个线程多次获得的情况
    偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能。

  • 偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连CAS操作都不做了,直接提高程序性能

如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,
竞争成功,表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级. 仍然为偏向锁;
竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。

引入依赖用于查看对象内存情况

<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>

打印看看偏向锁
在这里插入图片描述
这里是101就是偏向锁
但默认开启的偏向锁会有四秒延迟,所以我们测试的时候先让线程在new对象之前睡了5秒钟

public class SynchronizedUpDemo01 {
    public static void main(String[] args) {

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object o = new Object();
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

如果没有这个睡眠,出现的就是轻量级锁
轻量级锁对应的是000
在这里插入图片描述

偏向锁的撤销
当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁
竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会找行任何代码)撤销偏向锁。

我们看一张图,来理解一下
在这里插入图片描述
开始是无锁状态,检查Mark Word是否存放线程ID,是的话获得偏向锁,执行方法,没有存放的话通过CAS操作替换线程ID,CAS替换成功,执行方法,失败的话就要开始撤销偏向锁
在这里插入图片描述
原持有偏向锁的线程到达安全点,发生stop the world,会检查持有偏向锁的状态,当已经退出执行方法后,原持有偏向锁的线程会释放锁,恢复线程运行。
在这里插入图片描述
升级为轻量级锁,原来持有偏向锁的线程获得轻量级锁,从安全点继续执行方法。

轻量级锁

有线程来参与锁的竞争,但是获取锁的冲突时间极短
本质就是自旋锁CAS
一个在里面,一个在交替
总之不要轻易升级到阻塞
在这里插入图片描述
交替使用不升级
但是自旋多了就要升级
自适应自旋
线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。
反之如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。

  • 争夺轻量级锁失败时,自旋尝试抢占锁
  • 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁

重量级锁

在这里插入图片描述
Lock Record和互斥量
问题来了
锁升级后,hashcode去哪了
在这里插入图片描述
我们回顾一下

  • 偏向锁:MarkWord存储的是偏向的线程ID
  • 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针
  • 重量锁:MarkWord存储的是堆中Monitor对象的指针

当你在偏向锁里想拿hashcode,对不起,只能升级为重量级锁才能给你hashcode。。

不过轻量级锁中,可以拿hashcode
在这里插入图片描述
当一个无锁状态拿了hashcode,就没办法升级到偏向锁了,会直接升级到轻量级锁

优点缺点使用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争会带来额外的锁撤销的消耗适用于只有一个线程访问同步块的场景
轻量级锁(采用的是自旋锁)竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较长

锁消除和锁粗化

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

比如Vector和StringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能

假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁依用即可,避免次次的申请和释放锁,提升了性能

public class SynchronizedUpDemo01 {
static Object o = new Object();
    public static void main(String[] args) {
		  new Thread(() -> {
        Object o = new Object();
        synchronized (o) {
            System.out.println(1111);
        }
        synchronized (o) {
            System.out.println(222);
        }
        synchronized (o) {
            System.out.println(132);
        }
        synchronized (o) {
            System.out.println(1121);
        }
        synchronized (o) {
            System.out.println(11);
        }
    }
   });
}

像这样的代码,编译器会优化为

public class SynchronizedUpDemo01 {
static Object o = new Object();
public static void main(String[] args) {
        new Thread(() -> {
            synchronized (o) {
                System.out.println(1111);
                System.out.println(222);
                System.out.println(132);
                System.out.println(1121);
                System.out.println(11);
            }
        });
    }
}

粗化了

AQS

抽象队列同步器
是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,
是重量级基础框架及整个JC体系的基石,主要用于解决锁分配给"谁"的问题
可以发现源码就是一个抽象类
在这里插入图片描述
很多类都是继承自这个抽象类
在这里插入图片描述

整体来说,就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(也就是下图的state)表示持有锁的状态。
在这里插入图片描述
虚拟双向队列

Java并发大神DougLee,提出统一规范并简化了锁的实现,将其抽象出来
屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的--------公共基础部分,也就是这个AQS抽象类
有阻塞必定需要排队,实现排队就需要队列

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。
这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。
它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过==CAS、自旋以及LockSupport.park()的方式,维护state变量(同步状态标识)==的状态,使并发达到同步的效果。

我们看看封装好的 一个个Node的内部源码
waitStatus,当前节点在队列的状态
在这里插入图片描述

公平与非公平锁

顾名思义,公平就按队列先进先出依次执行
非公平就是当有机会时,大家一起抢
在这里插入图片描述
我们可以看道ReentrantLock下面的源码

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

基于CAS来实现State状态的切换

抢得到使用,抢不到入队
入队后可以等,也可以放弃

结束时间:2022-09-18

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值