常见线程安全问题之复合操作

news2024/11/27 9:29:25

创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家!


复合操作的问题本质上和 TOCTOU 是一样的,如果有多个操作(如同一变量的读写)就有可能出现线程安全问题。

不过在本节我们要强调的是,即使每个操作本身是原子的,复合操作也不是原子的,这种情形有时候比较难一眼就认出来。

示例

这里以《Java 并发编程实战》第二章的“因式分解”代码为例:

 
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
  private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
  private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();

  public void service(ServletRequest req, ServletResponse res) {
    BigInteger i = extractFromRequest(req);
    if (i.equals(lastNumber.get())) {               // ①
      encodeIntoResponse(resp, lastFactors.get());  // ②
    } else {
      BigInteger[] factors = factorOf(i);
      lastNumber.set(i);                            // ③
      lastFactors.set(factors);                     // ④
      encodeIntoResponse(resp, factors);
    }
  }
}

这个例子中 lastNumber 用来记录上一次做过“因式分解”的数,lastFactors 存放上次因式分解的结果。service 中先判断 lastNumber 是否与请求的数相等,如果相等则使用存储的 lastFactors;反之不相等则需要重新计算因式分解,并把结果存入 lastNumber 与 lastFactors 中。

这里 lastNumber 与 lastFactors 都用了 AtomicReference,它们是 JUC 中的类,可以理解为已经达到了原子性、可见性与顺序性。所以代码中的 ①②③④ 处的 get set 都是原子的,只不过复合操作的问题是,即使每个操作都是原子的,操作整体也不是原子的。

这个示例比较精妙的地方在于它很符合我们的编码习惯,如果不仔细思考甚至都发现不了它存在线程安全问题。

问题时序

考虑线程 A 与线程 B 同时进入 else 语句,且分别需要求得 2 和 3 的因式分解,考虑下面的时序:

----------- Thread A ---------------+--------- Thread B -----------------
lastNumber.set(i);         (=2)     |
                                    | lastNumber.set(i);         (=3)
                                    | lastFactors.set(factors);  (=[1,3])
lastFactors.set(factors);  (=[1,2]) |

则最终结束后 lastNumber = 3lastFactors = [1,2],则下次请求如果是分解 3 ,则会使用 lastFactors 的值,得到结果 [1,2],是错误的结果。

另一方面,也有可能是这样的时序:

----------- Thread A ---------------+--------- Thread B -----------------
lastNumber.set(i);         (=2)     |
                                    | if (i.equals(lastNumber.get()))  (= 2)
                                    |   encodeIntoResponse(resp, lastFactors.get());
lastFactors.set(factors);  (=[1,2]) |

这个时序里,一个线程计算了 2 的结果,正在写回缓存,过程中另一个线程请求因式分解 2,此时 lastNumber = 2,因此返回了 lastFactors 的内容,但线程 A 关于 2 的结果还未写回 lastFactors,线程 B 返回了一个错误的结果。

当然,也有可能是这样的时序:

----------- Thread A ---------------+--------- Thread B -----------------
Initial Value of lastNumber: 2      |
                                    | if (i.equals(lastNumber.get()))  (= 2)
lastNumber.set(i);         (=3)     |
lastFactors.set(factors);  (=[1,3]) |
                                    |   encodeIntoResponse(resp, lastFactors.get()); (=[1,3])

不成熟的解法:同步方法

从 TOCTOU 一节中我们知道,要解决这种竞争问题,需要把对状态的检查与使用都变成原子的,最简单的方式就是在方法上用 synchronized

 
@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
  // .. 省略代码

  public synchronized void service(ServletRequest req, ServletResponse res) {
    // .. 省略代码
  }
}

但是这个方法太极端了,所有的请求线程调用 service 方法都需要同步,同一时间只能有一个线程执行该方法,完全失去了多线程的优势。

解法:减小粒度

给整个方法加锁十分简单,但是由于锁的粒度很粗,并发性差。而我们的真实需求其实有两个:

  1. 对 lastNumber 和 lastFactors 的赋值操作需要是原子的
  2. 对 lastNumber 和 lastFactors 的读取也需要是原子的(至少读取过程中不允许赋值)

因此我们可以用 synchronized 代码块,实现如下:

 
public class UnsafeCachingFactorizer implements Servlet {
  private BigInteger lastNumber;
  private BigInteger[] lastFactors;

  public void service(ServletRequest req, ServletResponse res) {
    BigInteger i = extractFromRequest(req);
    BigInteger[] factors = null;

    synchronized (this) {                   // ①
      if (i.equals(lastNumber.get())) {
        factors = lastFactors.clone();
      }
    }

    if (factors == null) {
      factors = factor(i);
      synchronized (this) {                 // ②
        lastNumber = i;
        lastFactors = factors;
      }
    }

    encodeIntoResponse(resp, factors);
  }
}

在 ① 中把读操作用 synchronized 代码块保证原子性,在 ② 中用同样方法保证赋值的原子性。另一个关键点是,两个代码块需要加同一个锁,此处直接用了 this,是最稳妥的选择,当然也可以锁其它的 object,只要两个块加同一个锁即可。

另外此处因为使用了 synchronized,对 lastNumber 和 lastFactors 不再需要使用原子类。通常原子类(如 AtomicReference) 对单个操作的原子性保证很方便,但复合操作本身需要加锁,这里再使用原子类就显得没必要了。

小结

复合操作即使操作本身是原子的,复合操作作为一个整体本身也不具备原子性。所以和 TOCTOU 问题一样,解决方法是需要加锁来保证复合操作整体的原子性。

还有一点比较特殊,是我们看到“读操作”和“写操作”一样,都是必须要加锁的。

示例中我们也看到,并发编程是在简单性与并发性中的权衡。锁的粒度粗了,使用起来简单,但是并发性低,也许就满足不了性能要求;反之锁的粒度细了,并发性提高了,但是复杂度也随之增加,稍有不慎就容易有线程安全问题。

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

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

相关文章

【拥抱AI】如何查看Milvus的使用情况?

查看Milvus的使用情况和性能指标可以帮助你了解数据库的健康状况、性能指标和资源使用情况。以下是一些常用的方法和工具&#xff0c;帮助你全面监控和查看Milvus的使用情况和性能指标。 1. 查看日志 Milvus的日志文件记录了运行时的各种信息&#xff0c;包括错误、警告和调…

基于Netty实现聊天室

前言 了解了Netty的基本功能和相关概念&#xff0c;使用基于Netty实现多人聊天的功能。 需求 1.服务端能够接收客户端的注册&#xff0c;并且接受用户的信息注册 2.服务端能够处理客户端发送的消息&#xff0c;并且根据消息类型进行私发或者广播发送消 3.服务端能够私发消…

利用 Jsoup 进行高效 Web 抓取与 HTML 处理

Jsoup 是一款 Java 的 HTML 解析器&#xff0c;可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API&#xff0c;可通过 DOM&#xff0c;CSS 以及类似于 JQuery 的操作方法来取出和操作数据。 官网&#xff1a;https://jsoup.org/ 中文文档&#xff1a;Jsou…

【c语言】文件操作详解 - 从打开到关闭

文章目录 1. 为什么使用文件&#xff1f;2. 什么是文件&#xff1f;3. 如何标识文件&#xff1f;4. 二进制文件和文本文件&#xff1f;5. 文件的打开和关闭5.1 流和标准流5.1.1 流5.1.2 标准流 5.2 文件指针5.3 文件的打开和关闭 6. 文件的读写顺序6.1 顺序读写函数6.2 对比一组…

004 逻辑变量与运算

当0和1表示逻辑状态时&#xff0c;两个二进制数码按照某种特定的因果关系进行的运算——就叫&#xff1a;逻辑运算 1.二值逻辑变量与基本逻辑运算 逻辑代数: 与普通代数不同,逻辑代数中的变量只有0和1两个可取值&#xff0c;它们分别用来表示完全两个对立的逻辑状态 逻辑运…

Deepnote、JupyterLab、Google Colab、Amazon SageMaker、VS Code对比

功能比较 平台语言支持扩展性数据连接可视化能力DeepnotePython、R、SQL中等&#xff0c;依赖云端支持主要云平台&#xff08;BigQuery、Snowflake等&#xff09;内置仪表盘与交互图表JupyterLab多种语言&#xff0c;插件支持广泛极高&#xff0c;完全可自定义使用库&#xff…

网络安全中的数据科学如何重新定义安全实践?

组织每天处理大量数据&#xff0c;这些数据由各个团队和部门管理。这使得全面了解潜在威胁变得非常困难&#xff0c;常常导致疏忽。以前&#xff0c;公司依靠 FUD 方法&#xff08;恐惧、不确定性和怀疑&#xff09;来识别潜在攻击。然而&#xff0c;将数据科学集成到网络安全中…

C语言数据结构与算法--简单实现队列的入队和出队

&#xff08;一&#xff09;队列的基本概念 和栈相反&#xff0c;队列(Queue)是一种先进先出&#xff08;First In First Out&#xff09;的线性表。只 允许在表的一端进行插入&#xff0c;而在另一端删除元素&#xff0c;如日常生活中的排队现象。队列中 允许插入的一端叫队尾…

快速理解微服务中Sentinel怎么实现限流

Sentinel是通过动态管理限流规则&#xff0c;根据定义的规则对请求进行限流控制。 一.实现步骤 1.定义资源&#xff1a;在Sentinel中&#xff0c;资源可以是URL、方法等&#xff0c;用于标识需要进行限流的请求&#xff1b;(在Sentinel中&#xff0c;需要我们去告诉Sentinel哪些…

matlab根据excel表头筛选表格数据

有如下表格需要筛选&#xff1a; 如果要筛选style中的A&#xff0c;color中的F2&#xff0c;num中的3。 代码如下&#xff1a; clear;clc; file_Pathstrcat(F:\csdn\,test1.xlsx); %表格路径、文件名 E1readtable(file_Path,Sheet,1); %读取表格中的字母和数字,1代表第一个…

学习日志016--python实现双向循环列表与链栈

python中一些复合数据结构通过类的封装来实现的。双向循环链表与链栈也在其中。 双向循环链表 双向循环链表是一种特殊类型的链表&#xff0c;它结合了双向链表和循环链表的特点。在双向循环链表中&#xff0c;每个节点不仅包含数据&#xff0c;还持有指向前一个和后一个节点的…

【Docker】常用命令汇总

Docker 是1个开源的应用容器引擎&#xff0c;基于Go 语言并遵从 Apache2.0 协议开源。 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。 容器是完全使用沙箱机制&#xff0c;相…

QT QRadioButton控件 全面详解

本系列文章全面的介绍了QT中的57种控件的使用方法以及示例,包括 Button(PushButton、toolButton、radioButton、checkBox、commandLinkButton、buttonBox)、Layouts(verticalLayout、horizontalLayout、gridLayout、formLayout)、Spacers(verticalSpacer、horizontalSpacer)、…

Docker部署mysql:8.0.31+dbsyncer

Docker部署mysql8.0.31 创建本地mysql配置文件 mkdir -p /opt/mysql/log mkdir -p /opt/mysql/data mkdir -p /opt/mysql/conf cd /opt/mysql/conf touch my.config [mysql] #设置mysql客户端默认字符集 default-character-setUTF8MB4 [mysqld] #设置3306端口 port33…

[SUCTF 2019]EasySQL--详细解析

信息搜集 进入界面是一个搜索框&#xff1a; 查看一下源代码&#xff0c;显示是POST传参&#xff1a; 随便上传个数字1&#xff1a; 抓包测试一下闭合&#xff0c;发现以双引号闭合会回显nonono,单引号闭合则无回显。 由于没有报错信息&#xff0c;所以我们不能确定具体的闭…

警钟长鸣,防微杜渐,遨游防爆手机如何护航安全生产?

近年来&#xff0c;携非防爆手机进入危险作业区引发爆炸的新闻屡见报端。2019年山西某化工公司火灾&#xff0c;2018年延安某煤业瓦斯爆炸&#xff0c;均因工人未用防爆手机产生静电打火引发。涉爆行业领域企业量大面广&#xff0c;相当一部分企业作业场所人员密集&#xff0c;…

【智能流体力学】RAG大模型方法:解决固体力学和流体动力学问题

【使用 AutoGen + GPT-4o + Chainlit UI 进行工程仿真的对话式多智能体 AI 聊天机器人】 本项目构建了一个由多个AI代理组成的系统,这些代理通过使用Microsoft AutoGen进行对话交互,能够自主地创建和仿真固体力学(FEA)和流体动力学(CFD)问题。每个AI代理都擅长规划、问题…

Redis与MySQL如何保证数据一致性

Redis与MySQL如何保证数据一致性 简单来说 该场景主要发生在读写并发进行时&#xff0c;才会发生数据不一致。 主要流程就是要么先操作缓存&#xff0c;要么先操作Redis&#xff0c;操作也分修改和删除。 一般修改要执行一系列业务代码&#xff0c;所以一般直接删除成本较低…

Java项目实战II基于微信小程序的校运会管理系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导 一、前言 在充满活力与激情的校园生活中&#xff0c;校运会不仅是…

【西瓜书】神经网络-MP神经元、感知机和多层网络

神经网络&#xff08;neural networks&#xff09;的定义&#xff1a;神经网络是由具有适应性的简单单元组成的广泛并行互联的网络&#xff0c;它的组织能够模拟生物神经系统对真实世界物体所作出的交互反应。&#xff08;T. Kohonen 1988年在Neural Networks创刊号上给出的定义…