• 面试篇-Java-1+锁+AQS+死锁



    前言

    你们在项目中高并发时你都用过哪些锁,它的原理是什么;你知道类加载的过程,双亲委派机制吗;你们有用到过线程池吗,它的工作过程是怎样的。本文重点对面试的问题进行介绍,祝愿每位程序员都能顺利上岸!!!


    一、并发编程中你都用过哪些锁

    1.1 Synchronized【对象锁】

    1.1.1 Synchronized的使用

    Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住.具体的使用场景可以参考: 还在为Synchronized烦恼吗?那么请移步Java并发编程数据安全之Synchronized篇

    在这里插入图片描述

    1.1.2 你知道Synchronized的原理吗

    被Synchronized 关键字修饰的方法,代码块,在编译后的class 文件后,会通过monitorenter为对象上锁,通过monitorexit 释放锁,来实现排它锁。

    在这里插入图片描述

    1.1.2.1 你知道monitor 的结构

    monitor 数据结构中保存了当前获取所得线程,和等待去获取所得线程

    在这里插入图片描述

    1.1.3 Synchronized 的实现原理

    • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
    • 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现)线程获得锁需要使用对象(锁)关联monitor
    • 在monitor内部有三个属性,分别是owner、entrylist、waitset
    • 其中owner是关联的获得锁的线程,并且只能关联一个线程
    • ;entrylist关联的是处于阻塞状态的线程;
    • waitset关联的是处于Waiting状态的线程

    1.1.3 你知道Synchronized 锁的升级吗

    monitor 实现了重量级锁,只有发生并发资源的抢占,就会当前未抢到锁的线程,放入到阻塞队列中,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。引入了偏向锁和轻量级锁 此时锁的实现是怎样的呢;

    1.1.3.1 你知道对象头的内存结构和对象头的结构吗

    在这里插入图片描述

    对象头的结构
    在这里插入图片描述

    1.1.3.2 偏向锁

    线程1 进入,发现此时无锁,通过CAS 操作将线程1的id 设置到对象头中,并且将锁的标记设置为偏向锁;一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。

    在这里插入图片描述

    1.1.3.2 轻量级锁

    线程2此时进入,进入后发现为偏向锁,则通过CAS操作 将线程2的id 设置到对象头中;CAS 成果则获取到锁,如果CAS 失败,则进行偏向锁的撤销,并将锁升级为轻量级锁;线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。

    在这里插入图片描述

    1.1.3.3 重量级锁

    线程2 则通过CAS 操作将对象头的Markword设置为指向该帧中保存的markword副本,如果设置成功,则说明成功获取锁,如果获取失败,则自旋等待获取锁(当自旋多次依旧没有获取到锁时。会将轻量级锁升级为重量级锁(设置对象的锁标记为重量级锁))底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

    二、你知道CAS 吗

    CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。因为没有加锁,所以线程不会陷入阻塞,效率较高;如果竞争激烈,重试频繁发生,效率会受影响。
    在JUC(java.util.concurrent )包下实现的很多类都用到了CAS操作

    • AbstractQueuedSynchronizer(AQS框架)
    • A AtomicXXX类

    2.1 你知道CAS的执行流程吗:

    一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
    在这里插入图片描述

    当线程A 已经把a的值修改为101 后,线程B 此时修改值为99 就会失败
    在这里插入图片描述

    此时重新获取a的值,然后在次进行比较和交换
    在这里插入图片描述

    2.2 你知道CAS的底层实现吗:

    底层通过调用Unsafe 类中被native 修饰的方法进行实现;
    在这里插入图片描述

    2.3 你说下Synchronized 和CAS的区别

    • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
    • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

    三、你知道JMM,你们项目中有使用过volatile吗

    2.1 JMM Java 的内存模型

    JMM(lava Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。

    在这里插入图片描述

    你谈谈JMM

    • JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
    • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
    • 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

    2.2 你知道volatile 吗

    使用volatile 关键字修饰的属性,可以打破内存屏障,使得一个线程对其进行修改后,保证其它线程的可见性。一旦一个共享变量(类的成员变量、类的静态成员变量)被volatie修饰之后,那么就具备了两层语义

    • ① 保证线程间的可见性
    • ② 禁止进行指令重排序

    2.2.1 你知道JVM的JIT(即时编译器)

    JIT 在将代码进行编译时,会对代码优化,从而导致代码执行的先后顺序发生改变,从而在特定条件下发生问题。此时就可以使用volatile 关键字。
    在这里插入图片描述

    2.2.2 volatile 保证线程的可见性

    在这里插入图片描述

    2.2.3 你知道volatile的实现原理吗

    volatile禁止指令重排序,用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阴止其他读写操作越过屏障,从而达到阳止重排序的效果.

    发生指令重排序
    在这里插入图片描述
    使用volatile 禁止指令的重排序
    在这里插入图片描述
    你们使用volatile都遵从哪些规则

    • 写变量让volatile修饰的变量的在代码最后位置
    • 读变量让volatile修饰的变量的在代码最开始位置

    四、你知道AQS吗

    4.1 什么是AQS

    全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架
    AQS与Synchronized的区别

    在这里插入图片描述

    4.2 请你描述下AQS 的工作机制

    • AQS是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的像ReentrantLock、Semaphore都是基于AQS实现的
    • AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
    • 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性,如果CAS 修改成功,代表获取了锁,可以继续业务的执行,否则将当前线程放入到阻塞队列中,等待锁获取
    • 当持有锁的线程释放了锁,则从队列中唤醒头部元素进行锁的获取;

    在这里插入图片描述

    4.3 你都有过Lock 的哪些锁

    我在项目中使用到了ReentrantLock 锁,来对资源并发修改情况下进行加锁,ReentrantLock 锁的使用和原理详解可以参考:JAVA并发编程–4.1理解ReentrantLock

    4.3.1 你知道ReentrantLock 实现的原理吗

    ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

    在这里插入图片描述

    ReentrantLock 的工作过程
    在这里插入图片描述

    • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
    • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
    • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
    • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

    4.3.2 synchronized和Lock有什么区别

    语法层面:
    synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现Lock 是接口,源码由jdk 提供,用java 语言实现使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

    功能层面
    二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量Lock有适合不同场景的实现,如 ReentrantLock,ReentrantReadWriteLock(读写锁)

    性能层面
    在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖在竞争激烈时,Lock 的实现通常会提供更好的性能

    五、你遇到过死锁吗,你们是怎样排查的

    5.1 你了解死锁产生的条件吗

    当一个线程需要获取多把锁时,就有可能发生死锁。

    • 一个资源每次只能被一个线程使用
    • 一个线程在阻塞等待某个资源时,不释放已占有资源
    • 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
    • .若干线程形成头尾相接的循环等待资源关系
    • 这是造成死铁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死铁就需要打破第4个条件,不出现循环等待锁的关系。

    5.2 出现死锁后你们是怎么排查的

    涉及死锁的线程都会处于等待状态,虽然系统可能还未完全停止工作,但由于死锁线程占用了部分资源(如CPU时间片、内存等),系统的整体性能会受到影响,表现为响应速度变慢、吞吐量下降等,请求结构一直等待数据返回。

    我们通常使用jdk 自带的一些工具来进行排查:

    • 通过 jps:输出JVM中运行的进程状态信息
    • 通过jstack:查看iava进程内线程的维栈信息
    • 通过jconsole 连接到java 的应用程序,进行死锁检查
    • 通过jvisualvm 连接到java 的应用程序,进行死锁检查

    总结

    本文对Java 中进程内经常使用的锁,以及原理面试题进行汇总。

  • 相关阅读:
    Spring Boot 注解
    力扣用队列实现栈
    区块链技术优势和应用
    LeetCode·23.合并K个升序链表·递归·迭代
    springboot自动配置原理面试题(自用)
    [.NET项目实战] Elsa开源工作流组件应用(三):实战演练
    Angular:通过路由切换页面后,ngOnInit()不会被触发的问题
    完蛋,我被offer包围了|秋招自救指南
    外贸是什么意思?和跨境电商的区别是什么?
    VB-14
  • 原文地址:https://blog.csdn.net/l123lgx/article/details/140121004