【解析】ReentrantLock锁、Syschronized锁面试点解析

面试官提问

● 公平锁与非公平锁的区别是什么?

● 什么是可重入锁?

● 什么是死锁,怎样避免死锁?

● ReentrantLock与Syschronized实现原理是什么?两者有什么区别?

● 请说明ReentrantLock获取锁与释放锁的流程。

● 请说明Syschronized锁升级的过程。

● 锁性能优化方法是什么?

● 介绍一下AbstractQueuedSynchronizer(AQS)。

1 公平锁与非公平锁

公平锁是指多个线程竞争锁时直接进入队列排队,根据申请锁的顺序获得锁,先到先得。而非公平锁则是多个线程竞争锁时,首先尝试直接抢锁,失败后再进入等待队列。

使用公平锁,先到先得,线程获取锁时不会出现饥饿现象。使用非公平锁,整体的吞吐效率比较高。

ReentrantLock默认是非公平锁,在构造方法中传入参数true则为公平锁Synchronized是非公平锁

2 可重入锁

可重入锁是指一个线程可以多次获取同一把锁,其实现原理是,为每个锁关联一个计数器,线程首次获取锁时,计数器置为1,再次获取该锁时,计数器加1;线程每退出同步块一次,计数器就减1。计数器为0则代表锁被当前线程释放。

Synchronized和ReentrantLock都是可重入锁。

3 ReentrantLock锁

ReentrantLock锁的特点是可重入,支持公平锁和非公平锁两种方式。

阅读ReentrantLock代码可知,它主要利用CAS+AQS队列来实现。以非公平锁为例,当线程竞争锁时首先使用CAS抢占锁,成功则返回,失败则进入AQS队列并且挂起线程;当锁被释放时,唤醒AQS中的某个线程,从被挂起处再次尝试获取锁(当AQS队列头节点的下一个节点不为空时,直接唤醒该节点;否则从队尾向前遍历,找到最后一个不为空的节点并唤醒),获取锁失败则再次进入队尾。图1-13详细描述了ReentrantLock非公平锁的获取与释放流程。

图1-13 ReentrantLock非公平锁的获取与释放流程

下面通过源码来分析ReentrantLock的实现。非公平锁首先使用CAS检测锁是否空闲并抢占锁,当多个线程同时抢占同一把锁时,CAS操作保证只有一个线程执行成功。

final void lock() {//state为0则计数器设为1,表示抢占锁成功if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}

假设3个线程T1、T2和T3同时竞争锁,线程T1执行CAS成功,线程T2和T3则会进入acquire方法:

    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

接下来分别阅读tryAcquire、addWaiter和acquireQueued的实现代码。

进入tryAcquire方法,若锁空闲(state = 0),则当前线程通过CAS直接抢锁,抢锁成功则返回true;抢锁失败则判断持有锁的线程是否为自己,如果是自己的话就记录重入锁的次数,并返回获取锁成功,否则返回获取锁失败。

    protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();//锁处于空闲状态,没有被任何线程持有if (c == 0) {//忽略AQS队列中的等待线程,当前线程直接通过CAS抢锁体现了非公平性if (compareAndSetState(0, acquires)) {//抢锁成功,设置独占线程为当前线程setExclusiveOwnerThread(current);return true;}}//检查持有锁的线程是否为当前线程else if (current == getExclusiveOwnerThread()) {//可重入锁,记录重入次数int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}//获取锁失败return false;}

若tryAcquie获取锁失败,则执行addWaiter方法,线程加入AQS队列尾部,具体代码如下:

    private Node addWaiter(Node mode) {//初始化节点,模式设置为独占Node node = new Node(Thread.currentThread(), mode);Node pred = tail;//tail不为null,说明队列已被初始化if (pred != null) {node.prev = pred;//通过CAS将Node对象加入AQS队列,成为尾节点if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//队列未初始化或者CAS操作失败则进入enq函数enq(node);return node;}

T2和T3线程抢锁失败,假设它们同时加入AQS队列,由于队列尚未初始化(tail ==null),因此至少有一个线程进入enq()方法,代码如下:

这段代码通过自旋和CAS来实现非阻塞的原子操作,保证线程安全。假设T2和T3线程同时执行enq方法,**第一轮循环,CAS操作确保只有一个线程创建head节点;**第二轮循环,AQS队列完成初始化,tail非空,T2和T3线程都进入else逻辑,通过CAS操作将当前节点加入队尾。若T2线程执行compareAndSetTail成功,则T3线程需要在下一次循环时入队,最终AQS队列如图1-14所示。

T2和T3线程进入队列后执行acquireQueued()方法,AQS队列头节点的后继节点可以再次尝试获取锁,获取锁失败后被挂起,代码如下:

如果T1线程一直持有锁,那么T2和T3线程最终会进入shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法,代码如下:

最终T2和T3线程在状态为Node.SIGNAL的前驱节点后挂起,保证前驱节点获取锁后能唤醒自己。AQS队列中节点的状态及说明如表1-1所示。

表1-1 AQS节点的状态及说明

锁的释放过程比较简单,代码如下:

    public void unlock() {sync.release(1);}public final boolean release(int arg) {if (tryRelease(arg)) {//释放锁成功Node h = head;//唤醒AQS队列中的某个节点(一般是头节点)if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

核心方法是tryRelease和unparkSuccessor,先看一下tryRelease的执行过程,代码如下:

    protected final boolean tryRelease(int releases) {//重入锁,每重入一次则state加1,每释放锁一次则state 减1int c = getState() - releases;// 若当前线程不是持有锁的线程则抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;//state 减为 0,代表释放锁成功if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}

释放锁成功后会唤起AQS队列中被挂起的线程,代码如下:

    private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next;if (s == null || s.waitStatus > 0) {// 如果节点为null或者处于取消状态// 那就从后往前遍历寻找距离头节点最近的非取消节点s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 唤醒线程if (s != null)LockSupport.unpark(s.thread);
}

被唤醒的线程也不能保证抢锁成功,失败后依然会放置在队尾,这里也体现了锁的“非公平”性。

4 Syschronized锁

在HotSpot虚拟机中,对象内存布局主要分为对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),如图1-15所示。

当线程访问同步块时,首先需要获得锁并把相关信息存储在对象头中,对象头由以下两部分组成:

● Mark Word:存储自身运行时数据,例如HashCode、GC年龄、锁相关信息等内容。MarkWord信息结构如表1-2所示。

表1-2 Mark Word信息结构表

● Klass Pointer:Class对象的类型指针,指向的位置是对象对应的Class对象(对应的元数据对象)的内存地址。

总体上来说,锁升级过程如图1-16所示。

1)偏向锁

线程获取偏向锁的流程如下:

● 检查Mark Word中的线程id。

● 若线程id为空,则通过CAS设置为当前线程id:成功则获取锁,失败则撤销偏向锁。

● 若线程id不为空且为当前线程,则获取锁成功,否则撤销偏向锁

持有偏向锁的线程每次进入这个锁相关的同步块时,只需判断Mark Word中记录的线程id是否为自己。在没有竞争时,一个线程反复申请获得同一把锁,使用偏向锁效率极高。

2)轻量级锁

多个线程竞争偏向锁导致锁升级为轻量级锁,获取轻量级锁的流程如下:

● 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间LockRecord,并将对象头中的Mark Word复制到Lock Record。

● 利用CAS操作将对象头中的Mark Word更新为指向Lock Record的指针,若操作成功则竞争到锁,锁标志位变为00,表示当前为轻量级锁状 态。

● CAS执行失败且自旋一定次数后仍未成功,则说明该锁已被其他线程抢占,这时轻量级锁会膨胀为重量级锁,锁标志位变成为10。

使用轻量级锁提升性能的前提**:多个线程交替执行同步块,锁在整个生命周期内基本不会存在竞争或者出现锁竞争的概率很低,减少了使用重量级锁产生的性能消耗。**


轻量级锁与偏向锁的比较:轻量级锁每次申请、释放都至少需要一次CAS操作,但偏向锁只有在初始化时需要一次CAS操作,后续当前线程可以几乎零成本地直接获得锁(仅需比较线程id是否为自己)。


3)自旋锁

如果持有锁的线程能在很短时间内释放锁,那么竞争锁的线程就没有必要阻塞挂起,它们只需要自旋等待持有锁的线程释放锁,然后再尝试获取锁,这样就能减少传统的重量级锁因使用操作系统互斥量而产生的性能开销。因此,在轻量级锁膨胀为重量级锁之前,一般会尝试通过自旋的方式获取锁。假如当前持有锁的线程一直不释放锁,那么自旋就是在无意义地浪费CPU时间,当自旋多次始终无法获取锁时,轻量级锁会膨胀为重量级锁。

4)重量级锁

没有竞争到锁的线程会被挂起,只有在持有锁的线程退出同步块之后才会唤醒这些线程。唤醒操作涉及操作系统调度,开销很大。

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

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

相关文章

04.Python代码NumPy-通过索引或切片来访问和修改

04.Python代码NumPy-通过索引或切片来访问和修改 提示&#xff1a;帮帮志会陆续更新非常多的IT技术知识&#xff0c;希望分享的内容对您有用。本章分享的是Python基础语法。前后每一小节的内容是存在的有&#xff1a;学习and理解的关联性&#xff0c;希望对您有用~ python语法…

跨平台数据采集如何解决不同平台之间的数据兼容性问题?

在数字化时代&#xff0c;企业越来越依赖多个信息系统来管理业务&#xff0c;例如ERP&#xff08;企业资源计划&#xff09;、CRM&#xff08;客户关系管理&#xff09;、财务管理系统、电商平台等。然而&#xff0c;在进行跨平台数据采集时&#xff0c;不同系统之间的数据格式…

解决 vite.config.ts 引入scss 预处理报错

目录 报错1&#xff1a;[plugin:vite:css] [SASS] Error&#xff1a;Cant find stylesheet to import 报错2&#xff1a;[plugin:vite:css] [sass] Error: Undefined variable 版本号&#xff1a; "sass": "^1.86.3","sass-loader": "^1…

C++笔记,数学函数

参考链接&#xff1a;C中数学函数的使用方法_cpp里指数函数-CSDN博客 头文件 <cmath> 1. 基本的算数运算函数 1.1 sqrt() - 计算平方根 功能&#xff1a;计算一个非负实数的平方根。原型&#xff1a;double sqrt(double x);示例代码&#xff1a; #include <iostr…

不关“猫”如何改变外网IP?3种免重启切换IP方案

每次更换外网IP都要重启路由器&#xff1f;太麻烦了&#xff01;那么&#xff0c;不关猫怎么改变外网IP&#xff1f;无论是为了网络调试、爬虫需求&#xff0c;还是解决IP限制问题&#xff0c;频繁重启设备既耗时又影响效率。其实&#xff0c;更换外网IP并不一定要依赖“重启大…

道路运输安全员企业负责人考试内容与范围

道路运输企业主要负责人&#xff08;安全员&#xff09;考证要求 的详细说明&#xff0c;适用于企业法定代表人、分管安全负责人等需取得的 《道路运输企业主要负责人和安全生产管理人员安全考核合格证明》&#xff08;交通运输部要求&#xff09;。 考试内容与范围 1. 法律法…

深入剖析 WiFi 定位解析功能:原理、技术优势与应用场景

WiFi 定位解析功能的原理​ 信号强度与距离的关系​ WiFi 定位的核心原理基于无线信号传播过程中的一个基本特性&#xff1a;信号强度与信号发射源&#xff08;即 WiFi 接入点&#xff0c;Access Point&#xff0c;简称 AP&#xff09;和接收设备之间距离的关联。一般来说&am…

NVIDIA RTX™ GPU 低成本启动零售 AI 场景开发

零售行业正在探索应用 AI 升级客户体验&#xff0c;同时优化内部流程。面对多重应用场景以及成本优化压力&#xff0c;团队可采用成本相对可控的方案&#xff0c;来应对多重场景的前期项目预演和落地&#xff0c;避免短期内大规模投入造成的资源浪费。 客户体验 AI 场景的研究…

首次打蓝桥杯总结(c/c++B组)

目录 一、对每个题进行总结 1.填空题 2.第一个大题---可分解的正整数&#xff08;10--3&#xff09; 3.第二道大题---产值调整&#xff08;10--3&#xff09; 4.第三道大题---画展部署&#xff08;15--7&#xff09; 5.第四道大题---水质检测&#xff08;15--3&#x…

林纳斯·托瓦兹:Linux系统之父 Git创始人

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 林纳斯托瓦兹&#xff1a;Linux之父、Git创始人 一、传奇人物的诞生 1. 早年生活与家…

C语言多进程素数计算

题目描述&#xff1a; 以下代码实现了一个多进程素数计算程序&#xff0c;通过fork()函数创建子进程来并行计算指定范围内的素数。请仔细阅读代码并回答以下问题。 #include "stdio.h" #include "unistd.h" #include <sys/types.h> #include "…

uniapp-商城-27-vuex 通用方法

1 概述 上节说了vuex 的基本使用方法,分析了基本的使用方法。 在使用中,常见使用,我们要针对状态,购物车,不同类事务的管理,如果按照上节课的通用方法,那么使用和维护是会很大的难度的。 所以这里就必须要进行处理,借助 modules 进行定义不同类事务的处理手段。便于…

半导体设备通信标准—secsgem v0.3.0版本使用说明文档(4)之HSMS(SEMI E37)

文章目录 1、消息快1.1、选择 请求1.2、选择响应1.3、取消选择请求1.4、取消选择响应1.5、Linktest 请求1.6、Linktest 响应1.7、拒绝请求1.8、单独请求1.9、数据消息 2、 协议2.1、 事件 SEMI E37 HSMS 定义主机和设备之间通过 TCP 协议的通信。 它指定用于启动和终止连接的数…

通过GO后端项目实践理解DDD架构

最近在工作过程中重构的项目要求使用DDD架构&#xff0c;在网上查询资料发现教程五花八门&#xff0c;并且大部分内容都是长篇的概念讲解&#xff0c;晦涩难懂&#xff0c;笔者看了一些github上入门的使用DDD的GO项目&#xff0c;并结合自己开发中的经验&#xff0c;谈谈自己对…

Ubuntu系统连网问题

0. Preface 给一台新电脑装上Ubuntu系统后&#xff0c;接好网线&#xff0c;发现上不了网&#xff0c;右上角是有网络连接的图标的&#xff0c;也能获取到ip地址&#xff0c;就是没办法连网&#xff0c;ping www.google.com也没反应。 其实应该是网络设置有点问题&#xff0c;…

C/C++---头文件保护机制

在 C 和 C 编程里&#xff0c;头文件保护机制是一种防止头文件被重复包含的技术&#xff0c;它主要借助 #ifndef、#define 和 #endif 这些预处理指令来达成&#xff0c;也可以使用 #pragma once 这一编译器特定指令。下面详细阐述这一机制&#xff1a; 1. 头文件重复包含的问题…

蓝桥杯 8. 分巧克力

分巧克力 原题目链接 问题描述 儿童节那天有 K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。 小明一共有 N 块巧克力&#xff0c;其中第 i 块是 Hᵢ Wᵢ 的长方形。为了公平起见&#xff0c;小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。 要求…

从 SQL2API 到 Text2API:开启数据应用开发的新征程

在技术革新浪潮的席卷下&#xff0c;数据应用开发领域正经历着深刻变革。曾经&#xff0c;构建数据 API 需要开发者具备扎实的数据库知识和编程技能&#xff0c;手动编写复杂的 SQL 查询与 API 代码&#xff0c;这一过程不仅耗时费力&#xff0c;还将众多非技术人员阻挡在数据应…

继承:(开始C++的进阶)

我们今天来学习C的进阶&#xff1a; 面向对象三大特性&#xff1a;封装&#xff0c;继承&#xff0c;多态。 封装我们在前面已经学了&#xff0c;我们细细理解&#xff0c;我们的类的封装&#xff0c;迭代器的封装&#xff08;vector的迭代器可以是他的原生指针&#xff0c;li…

冒泡排序、插入排序、快速排序、堆排序、希尔排序、归并排序

目录 冒泡排序插入排序快速排序(未优化版本)快速排序(优化版本)堆排序希尔排序归并排序各排序时间消耗对比 冒泡排序 冒泡排序核心逻辑就是对数组从第一个位置开始进行遍历&#xff0c;如果发现该元素比下一个元素大&#xff0c;则交换位置&#xff0c;如果不大&#xff0c;就…