常见面试题之线程中并发锁(一)

news2025/1/11 8:44:31

1. 讲一下synchronized关键字的底层原理?

1.1. 基本使用

如下抢票的代码,如果不加锁,就会出现超卖或者一张票卖给多个人

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住

public class TicketDemo {

    static Object lock = new Object();
    int ticketNum = 10;


    public synchronized void getTicket() {
        synchronized (this) {
            if (ticketNum <= 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
            // 非原子性操作
            ticketNum--;
        }
    }

    public static void main(String[] args) {
        TicketDemo ticketDemo = new TicketDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                ticketDemo.getTicket();
            }).start();
        }
    }


}

1.2. Monitor

Monitor被翻译为监视器,是由jvm提供,c++语言实现

在代码中想要体现monitor需要借助javap命令查看clsss的字节码,比如以下代码:

public class SyncTest {

    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }
}

找到这个类的class文件,在class文件目录下执行javap -v SyncTest.class,反编译效果如下:

在这里插入图片描述

  • monitorenter 上锁开始的地方
  • monitorexit 解锁的地方
  • 其中被monitorentermonitorexit包围住的指令就是上锁的代码
  • 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁

在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁

monitor主要就是跟这个对象产生关联,如下图

在这里插入图片描述

Monitor内部具体的存储结构:

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取

  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程

  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

具体的流程:

  • 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待

参考回答:

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】

  • 它的底层由monitor实现的,monitorjvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor

  • monitor内部有三个属性,分别是ownerentrylistwaitset

  • 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

2. synchronized关键字的底层原理-进阶

Monitor实现的锁属于重量级锁,你了解过锁升级吗?

  • Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

  • JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

2.1. 对象的内存结构

HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充

在这里插入图片描述

我们需要重点分析MarkWord对象头

2.2. MarkWord

在这里插入图片描述

  • hashcode:25位的对象标识Hash

  • age:对象分代年龄占4位

  • biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁

  • thread:持有偏向锁的线程ID,占23位

  • epoch:偏向时间戳,占2位

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位

  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位

我们可以通过lock的标识,来判断是哪一种锁的等级

  • 后三位是001表示无锁
  • 后三位是101表示偏向锁
  • 后两位是00表示轻量级锁
  • 后两位是10表示重量级锁

2.3. 再说Monitor重量级锁

每个 Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

在这里插入图片描述

简单说就是:每个对象的对象头都可以设置monoitor的指针,让对象与monitor产生关联

2.4. 轻量级锁

在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。

static final Object obj = new Object();

public static void method1() {
    synchronized (obj) {
        // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized (obj) {
        // 同步块 B
    }
}

加锁的流程

1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

在这里插入图片描述

2.通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。

在这里插入图片描述

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。

在这里插入图片描述

4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。

解锁过程

1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record

2.如果Lock RecordMark Wordnull,代表这是一次重入,将obj设置为nullcontinue

在这里插入图片描述

3.如果Lock RecordMark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。

在这里插入图片描述

2.5. 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。

Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现

这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有

static final Object obj = new Object();

public static void m1() {
    synchronized (obj) {
        // 同步块 A
        m2();
    }
}

public static void m2() {
    synchronized (obj) {
        // 同步块 B
        m3();
    }
}

public static void m3() {
    synchronized (obj) {

    }
}

加锁的流程

1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

在这里插入图片描述

2.通过CAS指令将Lock Record的**线程id**存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。

在这里插入图片描述

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些

在这里插入图片描述

解锁流程参考轻量级锁

2.6. 参考回答

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

描述
重量级锁底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

一旦锁发生了竞争,都会升级为重量级锁

3. 你谈谈JMMJava内存模型)

JMM(Java Memory Model) Java内存模型,是java虚拟机规范中所定义的一种内存模型。

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

在这里插入图片描述

特点:

  1. 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

  2. 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

  3. 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

4. CAS你知道吗?

4.1. 概述及基本工作流程

CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。

JUCjava.util.concurrent)包下实现的很多类都用到了CAS操作

  • AbstractQueuedSynchronizerAQS框架)

  • AtomicXXX

例子:

我们还是基于刚才介绍过的JMM内存模型进行说明

  • 线程1与线程2都从主内存中获取变量int a = 100,同时放到各个线程的工作内存中

在这里插入图片描述

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

  • 线程1操作:Vint a = 100Aint a = 100B:修改后的值:int a = 101 (a++)
    • 线程1拿A的值与主内存V的值进行比较,判断是否相等
    • 如果相等,则把B的值101更新到主内存中

在这里插入图片描述

  • 线程2操作:Vint a = 100Aint a = 100B:修改后的值:int a = 99(a--)
    • 线程2拿A的值与主内存V的值进行比较,判断是否相等(目前不相等,因为线程1已更新V的值99)
    • 不相等,则线程2更新失败

在这里插入图片描述

  • 自旋锁操作

    • 因为没有加锁,所以线程不会陷入阻塞,效率较高

    • 如果竞争激烈,重试频繁发生,效率会受影响

在这里插入图片描述

需要不断尝试获取共享内存V中最新的值,然后再在新的值的基础上进行更新操作,如果失败就继续尝试获取新的值,直到更新成功

4.2. CAS底层实现

CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令

在这里插入图片描述

都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现

在这里插入图片描述

java中比较常见使用有很多,比如ReentrantLockAtomic开头的线程安全类,都调用了Unsafe中的方法

  • ReentrantLock中的一段CAS代码

在这里插入图片描述

4.3. 乐观锁和悲观锁

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

5. 请谈谈你对volatile的理解?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

5.1. 保证线程间的可见性

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。

一个典型的例子:永不停止的循环

package com.dcxuexi.basic;

// 可见性例子
// -Xint
public class ForeverLoop {
    static boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            System.out.println("modify stop to true...");
        }).start();
        foo();
    }

    static void foo() {
        int i = 0;
        while (!stop) {
            i++;
        }
        System.out.println("stopped... c:"+ i);
    }
}

当执行上述代码的时候,发现foo()方法中的循环是结束不了的,也就说读取不到共享变量的值结束循环。

主要是因为在JVM虚拟机中有一个JIT(即时编辑器)给代码做了优化。

上述代码

while (!stop) {
i++;
}

在很短的时间内,这个代码执行的次数太多了,当达到了一个阈值,JIT就会优化此代码,如下:

while (true) {
i++;
}

当把代码优化成这样子以后,及时stop变量改变为了false也依然停止不了循环

解决方案:

第一:

在程序运行的时候加入vm参数-Xint表示禁用即时编辑器,不推荐,得不偿失(其他程序还要使用)

第二:

在修饰stop变量的时候加上volatile,表示当前代码禁用了即时编辑器,问题就可以解决,代码如下:

static volatile boolean stop = false;

5.2. 禁止进行指令重排序

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

在这里插入图片描述

在去获取上面的结果的时候,有可能会出现4种情况

情况一:先执行actor2获取结果--->0,0(正常)

情况二:先执行actor1中的第一行代码,然后执行actor2获取结果--->0,1(正常)

情况三:先执行actor1中所有代码,然后执行actor2获取结果--->1,1(正常)

情况四:先执行actor1中第二行代码,然后执行actor2获取结果--->1,0(发生了指令重排序,影响结果)

解决方案

在变量上添加volatile,禁止指令重排序,则可以解决问题

在这里插入图片描述

屏障添加的示意图

在这里插入图片描述

  • 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
  • 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

其他补充

我们上面的解决方案是把volatile加在了int y这个变量上,我们能不能把它加在int x这个变量上呢?

下面代码使用volatile修饰了x变量

在这里插入图片描述

屏障添加的示意图

在这里插入图片描述

这样显然是不行的,主要是因为下面两个原则:

  • 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
  • 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/707083.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

常州工学院计算机组成原理(样卷)

微程序控制器&#xff1a;仿照程序设计的基本方法&#xff0c;将实现指令系统中所有指令所需要的所有控制信号按照一定的规则编码成微指令&#xff0c;若干条实现同一条指令功能的微指令构成一段微程序&#xff0c;将实现所有指令的微程序存放在一个只读存储器ROM中&#xff0c…

一套A股量化系统

shares A 股量化交易系统后台开发语言 Go/Python gmsec算法使用&#xff1a;pytorch全链路量化&#xff0c;行业板块分析&#xff0c;直接贴图。欢迎体验

【计算机视觉】最新综述:南洋理工和上海AI Lab提出基于Transformer的视觉分割综述

文章目录 一、导读二、摘要三、内容解读3.1 研究动机3.2 这篇综述的特色&#xff0c;以及与以往的Transformer综述有什么区别&#xff1f;3.3 Transformer-Based 分割和检测方法总结与对比3.4 相关研究领域的方法总结与对比3.5 不同方法的实验结果对比3.6 未来可以进行的方向 一…

vue三种方式导出报表至excel

1、序言 1.1、源码 源码在下方&#xff0c;复制运行&#xff0c;安装相应的插件即可 1.2、坑 方式一、方式二安装相同插件&#xff1a; npm install xlsx xlsx-style file-saver 导入 xlsx-style 会报如下的错误 解决办法&#xff1a; &#xff08;1&#xff09;去node_modules…

多元回归预测 | Matlab基于灰狼算法(GWO)优化混合核极限学习机HKELM回归预测, GWO-HKELM数据回归预测,多变量输入模型

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元回归预测 | Matlab基于灰狼算法(GWO)优化混合核极限学习机HKELM回归预测, GWO-HKELM数据回归预测,多变量输入模型 评价指标包括:MAE、RMSE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 …

基于matlab使用深度学习估计身体姿势(附源码)

一、前言 此示例演示如何使用 OpenPose 算法和预训练网络估计一个或多个人的身体姿势。 身体姿势估计的目标是识别图像中人的位置及其身体部位的方向。当场景中存在多个人时&#xff0c;由于遮挡、身体接触和相似身体部位的接近&#xff0c;姿势估计可能会更加困难。 有两种…

【手撕算法|动态规划系列No.1】leetcode1137. 第 N 个泰波那契数

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…

【C/C++】数组指针:array 地址 array *parray 两次解引用 **parray 值相同的原因解析

一、提出问题 #include <stdio.h>int main() { char array[16] {A, B}; char (*parray)[16] &array; printf("\n");printf(" array: \t%#lx\n", array); printf("& array: \t%#lx\n", &array); printf("…

SQL server 2012 配置数据库邮件实现邮件发送

日常开发中经常遇到邮件推送场景&#xff0c;我们可以利用SQL server也可以实现邮件发送功能。 一、配置邮件服务器 然后再弹出的页面中选择下一步 输入配置文件名&#xff0c;并添加新用户 在弹出的页面配置邮件服务器的地址、用户名、密码等相关信息 以上信息完成&#xff0…

树莓派 Raspberry Pi Zero 2W 安装默认系统时 ssh 登录并开启摄像头推流一段时间B

Raspberry Pi Zero 2W有点鸡肋&#xff0c;hdmi口用的microhdmi口&#xff0c;不是树莓派4b的minihdmi口&#xff0c;然后zero 2W也没有usb接口&#xff0c;有一个microusb安卓的otg接口&#xff0c;很烦&#xff0c;还好有wifi蓝牙模块&#xff0c;这样子还能ssh&#xff0c;不…

【冒泡排序】

前言 在计算机科学中&#xff0c;排序算法是一种常见且重要的算法。排序算法的目标是将一组无序的数据按照一定的规则进行重新排列&#xff0c;以便更方便地进行搜索、查找或其他操作。 冒泡排序&#xff08;Bubble Sort&#xff09;是最简单的排序算法之一&#xff0c;它的原…

基于Python+MySQL所写的商城管理系统

点击以下链接获取源码资源&#xff1a; https://download.csdn.net/download/qq_64505944/87971437?spm1001.2014.3001.5503 《51商城》程序使用说明 51商城项目分为网站前台和后台两个部分&#xff0c;下面将分别介绍这2个部分的使用。 1.网站前台 在虚拟环境中启动程序后&a…

Callback自定义测试-业务安全测试实操(23)

Callback自定义测试 测试原理和方法 在浏览器中存在着同源策略,所谓同源是指域名、协议、端口相同。当使用Aiax异步传输数据时,非同源域名之间会存在限制。其中有一种解决方法是JSONP (JSONwithPadding),基本原理是利用了HTML里<script></script>元素标签,远程…

Python 对象拷贝的那点事?

1.变量&#xff0c;引用和对象 变量无类型&#xff0c;它的作用仅仅在某个时候引用了特定的对象而已&#xff0c;具体在内存中就是一个指针&#xff0c;仅仅拥有指向对象的空间大小。 变量和对象的关系在于引用&#xff0c;变量引用对象后&#xff0c;也就对应了赋值的过程。…

VBA快速合并数据

实例需求&#xff1a;原始数据保存在工作表的A列至C列&#xff0c;现需要根据材料编号合并交付日期和交付数量&#xff0c;并且交付日期的日期格式采用两位数字年份简写格式&#xff0c;合并后的数据保存在E列和F列&#xff0c;如下图所示。 示例代码如下。 Sub demo()Dim o…

MSP430F249 Proteus仿真数码管秒表-0050

MSP430F249 Proteus仿真数码管秒表-0050 Proteus仿真小实验&#xff1a; MSP430F249 Proteus仿真数码管秒表-0050 功能&#xff1a; 硬件组成&#xff1a;MSP430F249单片机 2位数码管2个按键&#xff08;清零 开始/暂停&#xff09; 1.点击开始键后数码管开始秒表计时0~9…

Spring Cloud - HTTP 客户端 Feign 、自定义配置、优化、最佳实践

目录 一、Feign 是什么&#xff0c;有什么用呢&#xff1f; 二、Feign 客户端的使用 2.1、远程调用 1.引入依赖 2.在order-service&#xff08;发起远程调用的微服务&#xff09;的启动类添加注解开启Feign的功能 3.编写 Feign 客户端 4.通过 Feign 客户端发起远程调用 …

附件1.服务器操作系统安全加固要求及配置建议【上】

文章目录 加固文件说明【重启auditd服务后/etc/audit/audit.rules文件内容消失怎么处理】【用户的的本地登录和远程登录默认都会被审计&#xff1b; 2&#xff0c;可配置对chown、chmod、chcon等命令的执行进行审计&#xff1b;【ssh会话默认会被审计&#xff1b;】【可添加审计…