[多线程进阶] 常见锁策略

news2024/11/28 16:49:52


专栏简介: JavaEE从入门到进阶

题目来源: leetcode,牛客,剑指offer.

创作目标: 记录学习JavaEE学习历程

希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.

学历代表过去,能力代表现在,学习能力代表未来! 


目录:

1. 常见的锁策略

1.1 乐观锁 vs 悲观锁

1.2 读写锁:

1.3 重量级锁 vs 轻量级锁

1.4 自旋锁(Spin Lock)

1.5 公平锁 vs 非公平锁

1.6 可重入锁 vs 不可重入锁

          1.7 相关面试题


1. 常见的锁策略

锁策略之所以被叫做策略 , 是因为它并不是一个具体的锁 , 而是一系列供锁的实现者来参考的特性 , 对普通程序猿合理的使用锁也是有很大的帮助.

1.1 乐观锁 vs 悲观锁

乐观锁:

假设数据一般情况下不会产生并发冲突 , 所以在数据进行提交更新的时候 , 才会正式对数据是否产生并发冲突进行检测 , 如果发现并发冲突了 , 则返回错误的信息 , 让用户决定如果去做.

悲观锁:

总是假设最坏的情况 , 每次拿数据的时候都认为别人会修改 , 所以每次都会上锁 , 这样别人想拿数据就会阻塞直到它拿到锁.

例如: 同学A 和同学B 想请教老师一个问题:

  • 同学A 认为老师一定是比较忙的 , 因此他会先给老师法消息: "老师忙吗? 我上午10点向您请教一个问题."(相当于加锁操作)得到肯定回复后才会来 , 如果得到否定回复 , 那就等一段时间 , 下次再问老师. 这是个悲观锁
  • 同学B 认为老师一定是比较闲的 , 因此他会直接去找老师(没加锁 , 直接访问资源) , 如果老师确实比较闲 , 那么问题就解决了. 如果老师比较忙 , 那么也不会打扰老师下次再来(虽然每加锁 , 但能识别出数据访问冲突). 这是个乐观锁.

这两种思路的优劣要看具体的实现场景:

  • 如果当前老师确实比较忙 , 那么就适合使用悲观锁 , 使用乐观锁会导致"白跑很多趟" , 耗费额外的资源.
  • 如果当前老师比较闲 , 那么就适合使用乐观锁 , 使用悲观锁锁让效率

Synchronized 初始使用乐观锁策略 , 当发生锁竞争比较频繁时 , 就会自动切换成悲观锁策略.

同学C (相当于Synchronized)  , 开始认为"老师应该是比较闲的" , 有问题会直接去问老师.

但直接来找老师几次后 , 发现老师都挺忙的 , 于是下次来问老师会先发消息 , 在决定是否来问问题.


 乐观锁的一个重要功能就是检测出数据是发生访问冲突 , 我们可以引入一个版本号来解决.

假设需要多线程来修改"账户余额"

设当前账户余额为 100 , 引入一个版本号 version , 初始值为 1 , 并且我们规定"提交版本必须大于当前记录版本才能执行更新余额"

1) 线程 A 此时读出信息( version = 1 , balance = 100) , 线程 B 也读出信息(version = 1 , balance = 100)

2) 线程 A 操作的过程中并从其账户中扣除 50 (100-50) , 线程 B 从其账户中扣除 20(100-20).

3) 线程 A 完成修改工作 , 将数据版本号+1(version = 2) , 连同账户扣除余额(balance = 50)写到内存中.

4) 线程 B 完成操作 , 也将版本号+1(version = 2) , 尝试向内存中提交数据(balance = 80) , 但通过对比版本号发现 , 操作员 B 提交的数据版本号为 2 , 数据库记录的版本号也为 2 , 不满足"提交版本号必须大于记录当前版本才能执行更新" 的乐观锁策略. 于是认为这次操作失败.


1.2 读写锁:

多线程之间 , 数据的读取方之间不会产生线程安全 , 但数据的写入方之间以及读者之间都需要进行互斥 , 如果两种情况下都用同一个锁 , 就会产生极大的性能损耗 , 所以读写锁因此而产生.

读写锁(readers-writer lock) , 顾名思义 , 在执行加锁操作时需要额外表明读写意图 , 读者之间并不互斥 , 而写者则要求与任何人互斥.

一个线程对数据的访问 , 主要存在两种操作: 读数据 和 写数据

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁 , 这个对象提供了 lock / unlock 方法进行加锁操作.
  • ReentrantReadWriteLock.writeLock 类表示一个写锁 , 这个对象也提供了 lock / unlock 方法进行加锁解锁.

其中:

  • 加锁和加锁之间 , 不互斥.
  • 加锁和加锁之间 , 互斥.
  • 加锁和加锁之间 . 互斥.

Tips: 只要涉及到"互斥" , 就存在线程的挂起等待 , 一但线程被挂起 , 再次调用就不知在什么时候 , 因此尽可能的减少"互斥"的机会 , 就是提高效率的重要途径.

Synchronized 不是读写锁.


1.3 重量级锁 vs 轻量级锁

锁的核心特性是 " 原子性" , 这样的机制追更溯源是 CPU 这样的硬件设备提供的.

  • CPU 提供了"原子操作指令".
  • 操作系统基于 CPU 的原子指令 , 实现了 mutex 互斥锁.
  • JVM 基于操作系统提供的互斥锁 , 实现 synchronized ReentrantLock 等关键字和类.

 重量级锁: 加锁机制重度依赖 OS 提供了 mutex

  • 大量的内核态用户切换
  • 很容易引发线程的调度

这两个操作 , 成本比较高 , 一但涉及到用户态内核态的切换 , 就意味着"沧海桑田".

轻量级锁: 加锁机制尽可能不使用 mutex , 而是尽量在用户态代码完成 , 实在搞不定了 , 再使用 mutex.

  • 少量的用户态切换
  • 不容易引发线程调度

用户态 vs 内核态

假设去银行办理业务:

在窗口外 , 自己在 ATM 机办理业务就相当于用户态 , 用户态的时间成本是比较可控的.

在窗口内 , 工作人员办理 , 就是内核态 , 内核态的时间成本不可控.

如果办理业务需要和工作人员反复沟通 , 还需要重新排队 , 这样的效率是很低的.

synchronized 开始是一个轻量级锁 , 如果锁冲突较为严重 , 就会变成重量级锁. synchronized的轻量级锁是基于自旋锁实现的 , 重量级锁是基于挂起等待锁实现的.


1.4 自旋锁(Spin Lock)

按照上文的结论 , 线程在强锁失败后会进入阻塞状态 , 放弃 CPU , 需要过很久才能再次被调度.

但实际情况下 , 虽然强锁失败 , 但过不了多久锁就会被释放 , 没必要放弃 CPU , 此时就需要自旋锁来处理这样的问题.

自旋锁伪代码:

while(抢锁(lock) == 失败){}

如果获取锁失败 , 立即再次尝试获取锁 , 知道获取锁为止 , 第一次获取锁失败 , 第二次的尝试会在极短的时间内到来.

因此 , 一但锁被其他线程释放 ,  就能第一时间获取锁.

自旋锁 vs 挂起等待锁

当小明去找老师问题 , 老师说: 稍等一会 , 这会已经正在给其他同学讲题.

挂起等待锁: 回去干自己的事 , 过了很久很久之后 , 老师突然发来消息 , "这会有空闲时间"(注意 , 这个很长的时间间隔里 , 老师可能已经给多个同学讲完题了)

自旋锁: 站在老师办公室门口 , 一旦上个同学出来 , 那么就能立即抓住机会问题.

自旋锁是一种典型的 轻量级锁 的实现方式:

  • 优点: 没有放弃 CPU , 不涉及线程的阻塞的调度 , 一旦锁被释放 , 就能第一时间获取到锁
  • 缺点: 如果锁被其他线程占用时间过长 , 那么就会持续的消耗 CPU 资源.(挂起等待的时候不消耗 CPU 资源)

synchronized 中的轻量级锁就是通过自旋锁的方式形成.


1.5 公平锁 vs 非公平锁

假设有三个线程 A , B , C. A成功获取锁 , B和C都尝试获取锁 , 但获取失败 , 阻塞等待. 那么当 A 线程释放锁时 , 会发生什么?

公平锁: 遵循"先来后到". B 比 C 先来 , 那么当 A 释放后 , B 就可能先于 C 获得锁.

非公平锁:不遵循"先来后到". B 和 C 都有可能获得锁.

Tips:

  • 操作系统内部的线程调度就是随机的 , 如果不做任何额外的限制 , 锁就是非公平锁. 如果想要实现公平锁 , 就需要依赖额外的数据结构. 记录线程的先后顺序.
  • 公平锁与非公平锁没有好坏之分 , 关键在于适用场景.

synchronized 是非公平锁.


1.6 可重入锁 vs 不可重入锁

可重入锁的字面意思是 "可重新进入的锁" , 即允许一个线程多次获取同一把锁.

例如一个递归函数中有加锁操作 , 递归过程中如果锁不会阻塞自己 , 那么这个锁就是可重入锁(递归锁).

Java 中只要是以 Reentrant 开头的都是可重入锁 , 而且 JDK提供的所有现成的Lock实现类 , 包括synchronized 关键字都是可重入的.

而 LInux 系统提供的 mutex 是不可重入锁.

Synchronized 是可重入锁.


1.7 相关面试题

1. 你是怎么理解乐观锁和悲观锁的 , 具体怎么实现?

  • 悲观锁认为多个线程访问同一个共享变量冲突概率较大 , 会在每次访问共享变量前真正加锁.
  • 乐观锁认为多个线程访问同一个共享变量的冲突概率不大 , 并不会真的加锁 , 而是直接尝试访问数据. 在访问的同时识别当前数据是否发生访问冲突.
  • 乐观锁的实现可以引入一个版本号 , 借助版本号识别当前数据是否访问冲突.

2. 介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁.

读操作和读操作之间不互斥.

读操作和写操作之间互斥.

写操作和写操作之间互斥.

读写锁的最长用场景就是 "频繁读 , 不频繁写".

3. 什么是自旋锁 , 为什么要使用自旋锁策略 , 缺点是什么?

自旋锁是一种轻量级锁 , 如果获取锁失败 , 会无限循环不停的获取锁 , 直到获取到锁为止 , 因此一但锁被其他线程释放可以第一时间获取.

相比于挂起等待锁:

优点: 没有放弃 CPU 资源 , 一但锁被释放就能第一时间获取到锁 ,  更高效. 在锁持有时间比较短的情况下非常高效.

缺点: 如果锁长时间的持有机会浪费 CPU 资源.

4. synchronized 是可重入锁吗?

是可重入锁 , 可重入锁指的是连续两次加锁不会导致死锁.

实现方式是在锁中记录持有该锁的线程身份 , 以及一个计数器(记录加锁的次数) , 如果发现当前加锁的线程就是持有锁的线程 , 则直接计数器自增.

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

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

相关文章

bootstrap 框架

文章目录bootstrap必须使用 HTML5 文档类型排版和链接默认栅格系统带有基本栅格的 HTML 代码媒体类型媒体类型逻辑运算符 用来做条件判断页面布局: 引入 css(bootstrap.min.css) 类名03-面包屑导航警告框、徽章、面包屑导航、按钮、按钮组卡…

css行内块元素垂直居中

css行内块元素垂直居中 div里边有个img标签&#xff0c;要想让img垂直居中&#xff0c;需要 给父盒子设置line-heightheightimg设置vertical-align:middle <div style"background-color: red; height: 150px;line-height: 150px;"><img src"images/…

Unity开发环境配置

Unity本体安装 1.首先下载安装unityhub,中文管网https://unity.cn/ 2.登录unityhub&#xff0c;选择你想要的版本安装 选择后按照提示选择个人免费试用的license,然后等待unity本体下载安装即可。 VSCode安装和配置 1.去官网https://code.visualstudio.com/下载vscode 2.u…

微信小程序 Springboot ssm房屋租赁系统uniapp设计与实现

房屋租赁系统用户和户主是基于微信端&#xff0c;管理员是基于网页端&#xff0c;系统采用java编程语言&#xff0c;mysql数据库&#xff0c; idea工具开发&#xff0c;本系统分为用户&#xff0c;户主&#xff0c;管理员三个角色&#xff0c;其中用户可以注册登陆小程序&#…

C++11入门

目录 C11简介 统一的列表初始化 {}初始化 std::initializer_list 文档介绍 std::initializer_list的类型 使用场景 initializer_list接口函数模拟实现 auto与decltype nullptr 范围for STL的变化 新容器 新方法 新函数 C11简介 1.在2003年C标准委员会曾经提交了一…

【浅学Redis】缓存 以及 缓存穿透、缓存击穿、缓存雪崩

缓存 以及 缓存击穿、缓存穿透、缓存雪崩1. 缓存1.1 缓存的作用1.2 缓存的应用场景1.3 引入缓存后的执行流程1.4 缓存的优点2. 缓存穿透2.1 场景2.2 解决策略1. 参数校验2. 缓存空值3. 缓存击穿3.1 场景3.2 解决策略4. 缓存雪崩4.1 场景4.2 解决策略5. 上面三者的区别1. 缓存 …

图像分割--入门了解

一. 三种分割 1. 语义分割&#xff08;semantic segmentation&#xff09; 语义分割&#xff1a;语义分割通过对图像中的每个像素进行密集的预测、推断标签来实现细粒度的推理&#xff0c;从而使每个像素都被标记为一个类别&#xff0c;但不区分属于相同类别的不同实例。 比…

ChatGPT之父:世界正被他搅动

阿尔特曼&#xff08;左&#xff09;与马斯克Mac LC2电脑ChatGPT这款聊天应用程序最近太火了&#xff01; 美国北密歇根大学的一名学生用它生成了一篇哲学课小论文&#xff0c;“惊艳”了教授&#xff0c;还得到了全班最高分。美国一项调查显示&#xff0c;53%的学生用它写过论…

Vue (2)

文章目录1. 模板语法1.1 插值语法1.2 指令语法2. 数据绑定3. 穿插 el 和 data 的两种写法4. MVVM 模型1. 模板语法 root 容器中的代码称为 vue 模板 1.1 插值语法 1.2 指令语法 图一 &#xff1a; 简写 &#xff1a; v-bind: 是可以简写成 &#xff1a; 的 总结 &#xff1a; …

Springboot + RabbitMq 消息队列

前言 一、RabbitMq简介 1、RabbitMq场景应用&#xff0c;RabbitMq特点 场景应用 以订单系统为例&#xff0c;用户下单之后的业务逻辑可能包括&#xff1a;生成订单、扣减库存、使用优惠券、增加积分、通知商家用户下单、发短信通知等等。在业务发展初期这些逻辑可能放在一起…

【23种设计模式】创建型模式详细介绍

前言 本文为 【23种设计模式】创建型模式详细介绍 相关内容介绍&#xff0c;下边具体将对单例模式&#xff0c;工厂方法模式&#xff0c;抽象工厂模式&#xff0c;建造者模式&#xff0c;原型模式&#xff0c;具体包括它们的特点与实现等进行详尽介绍~ &#x1f4cc;博主主页&…

计算机组成原理(一)

1.了解计算机硬件的发展和软件的发展历程&#xff1b; 硬件&#xff1a;   电子管时代&#xff08;1946-1959&#xff09;&#xff1a;电子管、声汞延迟线、磁鼓   晶体管时代&#xff08;1959-1964&#xff09;&#xff1a;晶体管、磁芯   中、小规模集成电路时代&#…

OpenStack云平台搭建(1) | 基础环境准备

目录 一、环境准备 1.1、关闭selinxu 1.2、关闭防火墙 1.3、修改主机名 1.4、配置时间同步服务器 1.5、配置域名 二、安装OpenStack库 2.1、启用OpenStack仓库的包 2.2、安装python-openstackclient 2.3、controller安装数据库 2.4、安装消息队列 2.5、配置缓存 2.…

Linux驱动开发基础__中断的线程化处理

目录 1 引入 2 内核机制 2.1 调用 request_threaded_irq 后内核的数据结构 2.2 request_threaded_irq 2.3 中断的执行过程 1 引入 复杂、耗时的事情&#xff0c;尽量使用内核线程来处理。工作队列用起来挺简单&#xff0c;但是它有一个缺点&#xff1a;工作队列中有多个 …

【Java 面试合集】HashMap中为什么要使用红黑树

HashMap中为什么要使用红黑树1. 概述 从源码的结构方面讲述下为什么HashMap要使用红黑树。那没有红黑树的时候&#xff0c;底层是基于什么逻辑进行存储的。 2. 底层结构 如果忽略红黑树的话&#xff0c;HashMap底层的数据存储结构如下&#xff1a; 总体而言就是数组 链表的形…

Vscode使用

我是四五年的webstorm忠粉&#xff0c;一直觉得它是世界上最好用、强大、方便的编辑器。 为了它深谙各种破解方法&#xff0c;突然有一天我知道的几种方法都不奏效了。破解的实在太累了&#xff0c;算了&#xff0c;尝试尝试不同的工具吧。 含泪挥别webstrom&#xff0c;捏着…

JDBC编程复习

文章目录JDBC1.概念2.原理3. 如何使用JDBC编程1. 下载mysql的jdbc驱动2. 项目中引入驱动4. JDBC使用1. 和数据库建立连接2.获取连接3. Statement对象4. 释放资源JDBC 1.概念 JDBC,即Java Database Connectivity&#xff0c;java数据库连接。是Java提供的API用来执行SQL语句&a…

SWPU新生赛WriteUp

一个线上赛&#xff0c;这个NSSCTF最爽的就是没有靶机操作的一分钟冷却&#xff0c;10.11比赛结束&#xff0c;但是我还要看看工控&#xff0c;所以不打这个比赛了&#xff0c;先把wp写了&#xff0c;pwn入门真TM艰难 WEB 前面送分题&#xff0c;中间的也是基础题&#xff0c;…

SQL概述及数据定义

文章目录一、SQL概述1.简介2.特点3.组成4.SQL分类5.书写规范二、数据定义1.模式的定义与删除①定义模式②删除模式2.基本表的定义、删除与修改①定义基本表②数据类型③模式与表④修改基本表⑤删除基本表3.索引的建立与删除①建立索引②删除索引一、SQL概述 1.简介 SQL (Stru…

使用IDA查看汇编代码上下文去辅助排查C++软件异常问题

目录 1、概述 2、汇编指令能最直接反映异常崩溃的原因 2.1、查看异常Code码及对应的异常类型 2.2、查看发生崩溃的那条汇编指令 3、阅读汇编代码上下文需要掌握一定的基础汇编知识 4、Windbg中显示的函数调用堆栈中的C代码行号&#xff0c;和最新的代码对不上了 5、Wind…