Day827.安全性、活跃性以及性能问题 -Java 并发编程实战

news2024/11/25 14:21:58

安全性、活跃性以及性能问题

Hi,我是阿昌,今天学习记录的是关于安全性、活跃性以及性能问题的内容。

并发编程中需要注意的问题有很多,主要有三个方面,分别是:

  • 安全性问题
  • 活跃性问题
  • 性能问题

一、安全性问题

相信一定听说过类似这样的描述:

这个方法不是线程安全的,这个类不是线程安全的,等等。

那什么是线程安全呢?其实本质上就是正确性,而正确性的含义就是程序按照期望的执行,不要让感到意外。

在并发编程 Bug 的源头中,已经见识过很多诡异的 Bug,都是出乎预料的,它们都没有按照期望的执行。

那如何才能写出线程安全的程序呢?并发编程Bug的源头中介绍了并发 Bug 的三个主要源头:

  • 原子性问题
  • 可见性问题
  • 有序性问题

也就是说,理论上线程安全的程序,就要避免出现原子性问题、可见性问题和有序性问题。

那是不是所有的代码都需要认真分析一遍是否存在这三个问题呢?

当然不是,其实只有一种情况需要:存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据。那如果能够做到不共享数据或者数据状态不发生变化,不就能够保证线程的安全性了嘛。

有不少技术方案都是基于这个理论的,例如线程本地存储(Thread Local Storage,TLS)、不变模式等等,但是,现实生活中,必须共享会发生变化的数据,这样的应用场景还是很多的。

当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果不采取防护措施,那么就会导致并发 Bug,对此还有一个专业的术语,叫做数据竞争(Data Race)。

比如,前面并发编程Bug的源头里有个 add10K() 的方法,当多个线程调用时候就会发生数据竞争,如下所示。


public class Test {
  private long count = 0;
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
}

那是不是在访问数据的地方,加个锁保护一下就能解决所有的并发问题了呢?

显然没有这么简单。例如,对于上面示例,稍作修改,增加两个被 synchronized 修饰的 get() 和 set() 方法, add10K() 方法里面通过 get() 和 set() 方法来访问 value 变量,修改后的代码如下所示。

对于修改后的代码,所有访问共享变量 value 的地方,都增加了互斥锁,此时是不存在数据竞争的。

但很显然修改后的 add10K() 方法并不是线程安全的。


public class Test {
  private long count = 0;
  synchronized long get(){
    return count;
  }
  synchronized void set(long v){
    count = v;
  } 
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      set(get()+1)      
    }
  }
}

假设 count=0,当两个线程同时执行 get() 方法时,get() 方法会返回相同的值 0,两个线程执行 get()+1 操作,结果都是 1,之后两个线程再将结果 1 写入了内存。本来期望的是 2,而结果却是 1。这种问题,有个官方的称呼,叫竞态条件(Race Condition)。

所谓竞态条件,指的是程序的执行结果依赖线程执行的顺序。

例如上面的例子,如果两个线程完全同时执行,那么结果是 1;

如果两个线程是前后执行,那么结果就是 2。

在并发环境里,线程的执行顺序是不确定的,如果程序存在竞态条件问题,那就意味着程序执行的结果是不确定的,而执行结果不确定这可是个大 Bug。

下面再结合一个例子来说明下竞态条件,就是前面文章中提到的转账操作。

转账操作里面有个判断条件——转出金额不能大于账户余额,但在并发环境里面,如果不加控制,当多个线程同时对一个账号执行转出操作时,就有可能出现超额转出问题。

假设账户 A 有余额 200,线程 1 和线程 2 都要从账户 A 转出 150,在下面的代码里,有可能线程 1 和线程 2 同时执行到第 6 行,这样线程 1 和线程 2 都会发现转出金额 150 小于账户余额 200,于是就会发生超额转出的情况。


class Account {
  private int balance;
  // 转账
  void transfer(
      Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  } 
}

所以也可以按照下面这样来理解竞态条件。

在并发场景中,程序的执行依赖于某个状态变量,也就是类似于下面这样:


if (状态变量 满足 执行条件) {
  执行操作
}

当某个线程发现状态变量满足执行条件后,开始执行操作;

可是就在这个线程执行操作的时候,其他线程同时修改了状态变量,导致状态变量不满足执行条件了。

当然很多场景下,这个条件不是显式的,例如前面 addOne 的例子中,set(get()+1) 这个复合操作,其实就隐式依赖 get() 的结果。

那面对数据竞争和竞态条件问题,又该如何保证线程的安全性呢?

其实这两类问题,都可以用互斥这个技术方案,而实现互斥的方案有很多,CPU 提供了相关的互斥指令,操作系统、编程语言也会提供相关的 API。

从逻辑上来看,可以统一归为:


二、活跃性问题

所谓活跃性问题,指的是某个操作无法执行下去。

常见的“死锁”就是一种典型的活跃性问题,当然除了死锁外,还有两种情况,分别是“活锁”和“饥饿”。

发生“死锁”后线程会互相等待,而且会一直等待下去,在技术上的表现形式是线程永久地“阻塞”了。

但有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁”。

可以类比现实世界里的例子,路人甲从左手边出门,路人乙从右手边进门,两人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果是两人又相撞了。这种情况,基本上谦让几次就解决了,因为人会交流啊。

可是如果这种情况发生在编程世界了,就有可能会一直没完没了地“谦让”下去,成为没有发生阻塞但依然执行不下去的“活锁”。

解决“活锁”的方案很简单,谦让时,尝试等待一个随机的时间就可以了。

例如上面的那个例子,路人甲走左手边发现前面有人,并不是立刻换到右手边,而是等待一个随机的时间后,再换到右手边;

同样,路人乙也不是立刻切换路线,也是等待一个随机的时间再切换。由于路人甲和路人乙等待的时间是随机的,所以同时相撞后再次相撞的概率就很低了。

等待一个随机时间”的方案虽然很简单,却非常有效,Raft 这样知名的分布式一致性算法中也用到了它。


那“饥饿”该怎么去理解呢?

所谓“饥饿”指的是线程因无法访问所需资源而无法执行下去的情况。

“不患寡,而患不均”,如果线程优先级“不均”,在 CPU 繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;

持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。

解决“饥饿”问题的方案很简单,有三种方案:

  • 一是保证资源充足
  • 二是公平地分配资源
  • 三就是避免持有锁的线程长时间执行

这三个方案中,方案一和方案三的适用场景比较有限,因为很多场景下,资源的稀缺性是没办法解决的,持有锁的线程执行的时间也很难缩短。

倒是方案二的适用场景相对来说更多一些。那如何公平地分配资源呢?

在并发编程里,主要是使用公平锁。

所谓公平锁,是一种先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获得资源。


三、性能问题

使用“锁”要非常小心,但是如果小心过度,也可能出“性能问题”。

”的过度使用可能导致串行化的范围过大,这样就不能够发挥多线程的优势了,而我们之所以使用多线程搞并发程序,为的就是提升性能。

所以要尽量减少串行,那串行对性能的影响是怎么样的呢?

假设串行百分比是 5%,用多核多线程相比单核单线程能提速多少呢?

有个阿姆达尔(Amdahl)定律,代表了处理器并行运算之后效率提升的能力,它正好可以解决这个问题,具体公式如下:

在这里插入图片描述

公式里的 n 可以理解为 CPU 的核数,p 可以理解为并行百分比,那(1-p)就是串行百分比了,也就是假设的 5%。

再假设 CPU 的核数(也就是 n)无穷大,那加速比 S 的极限就是 20。也就是说,如果串行率是 5%,那么无论采用什么技术,最高也就只能提高 20 倍的性能

所以使用锁的时候一定要关注对性能的影响。 那怎么才能避免锁带来的性能问题呢?

这个问题很复杂,Java SDK 并发包里之所以有那么多东西,有很大一部分原因就是要提升在某个特定领域的性能。

不过从方案层面,可以这样来解决这个问题。

  • 第一,既然使用锁会带来性能问题,那最好的方案自然就是使用无锁的算法数据结构了。在这方面有很多相关的技术,例如线程本地存储 (Thread Local Storage, TLS)、写入时复制 (Copy-on-write)、乐观锁等;Java 并发包里面的原子类也是一种无锁的数据结构;Disruptor 则是一个无锁的内存队列,性能都非常好……
  • 第二,减少锁持有的时间。互斥锁本质上是将并行的程序串行化,所以要增加并行度,一定要减少持有锁的时间。这个方案具体的实现技术也有很多,例如使用细粒度的锁,一个典型的例子就是 Java 并发包里的 ConcurrentHashMap,它使用了所谓分段锁的技术(这个技术后面我们会详细介绍);还可以使用读写锁,也就是读是无锁的,只有写的时候才会互斥。

性能方面的度量指标有很多,觉得有三个指标非常重要,就是:吞吐量、延迟和并发量。

  1. 吞吐量:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。
  2. 延迟:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。
  3. 并发量:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒。

四、总结

并发编程是一个复杂的技术领域,微观上涉及到原子性问题、可见性问题和有序性问题,宏观则表现为安全性、活跃性以及性能问题。在设计并发程序的时候,主要是从宏观出发,也就是要重点关注它的安全性、活跃性以及性能。

安全性方面要注意数据竞争和竞态条件,活跃性方面需要注意死锁、活锁、饥饿等问题,性能方面虽然介绍了两个方案,但是遇到具体问题,还是要具体分析,根据特定的场景选择合适的数据结构和算法。

要解决问题,要把问题分析清楚。


Java 语言提供的 Vector 是一个线程安全的容器,写了下面的代码,看看是否存在并发问题呢?


void addIfNotExist(Vector v, 
    Object o){
  if(!v.contains(o)) {
    v.add(o);
  }
}

vector是线程安全,指的是它方法单独执行的时候没有并发正确性问题,并不代表把它的操作组合在一起问木有,而这个程序显然有竞态条件问题。

检查是否存在和加入这个动作要求是原子性的,不能拆分


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

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

相关文章

论文讲解p2p4

本人水平有限,很多地方可能有说错或者理解错的地方请指出,谢谢谅解 一.原始电路图 1.简介:本电路是从1977年一篇电荷重新分配的理念进化而来,论文如下: All-MOS charge redistribution analog-to-digital conversion techniques. --JAMES L. McCREARY 2.因为从一开始充电过…

Java使用H2数据库全方式汇总

H2是轻量级数据库&#xff0c; 可以不需要安装就可以运行&#xff0c;对于快速学习和演示比较适用。关于H2的基本内容可以参考&#xff1a; H2 数据库简介 。 本篇快速介绍H2数据库在各种类型的Java应用中的使用&#xff0c; 包括&#xff1a; Java 项目Java Web 项目Spring B…

《域渗透攻防指南》签名版预售来啦

千呼万唤始出来&#xff01;终于&#xff0c;在广大粉丝翘首期盼下&#xff0c;国内首本专门讲述域内攻防的书籍《域渗透攻防指南》在2022年最后一个月和大家见面了。为了回馈粉丝的等待&#xff0c;让粉丝早日拿到心仪的书&#xff0c;特此联合机械工业出版社弄了签名版书预售…

GLAD:部分相干光模拟

概述 一个理想的单色点光源发射的光是完全相干光。但实际物理光源不是点源&#xff0c;总是具有一定的空间尺度并包含众多辐射单元&#xff0c;其发出的光也非严格的单色光,其光谱具有一定宽度&#xff0c;这种光即部分相干光。产生部分相干光主要有三种方法&#xff1a; …

Python中常用的内置函数集合

这篇文章主要介绍了Python中常用的内置函数&#xff0c;主要介绍内容有map()、filter()、all()、int()等更多相关函数&#xff0c;需要的小伙伴可以看看。 一、map() map(func,iterable)&#xff0c;其中func为函数名&#xff0c;可为lambda匿名函数&#xff0c;iterable为可迭…

全栈Jmeter接口测试(二):jmeter组件元件介绍,利用取样器中http发送请求

JMeter 的主要测试组件总结如下&#xff1a; 1. 测试计划是使用 JMeter 进行测试的起点&#xff0c;它是其它 JMeter 测试元件的容器 2. 线程组代表一定数量的并发用户&#xff0c;它可以用来模拟并发用户发送请求。实际的 请求内容在Sampler中定义&#xff0c;它被线程组包含…

Camtasia Studio2023电脑屏幕录制软件免费版

TechSmith Camtasia Studio2023免费的屏幕录像视频编辑软件&#xff0c;最专业的电脑屏幕录制及编辑软件&#xff01;这款专业录屏与视频创作大型软件包含屏幕录像、视频编辑、视频菜单制作、视频录音配音、视频发布等系列强大功能。 全新的Camtasia 2023 让您用更短的时间创…

【图像隐写】GBT+SVD数字水印嵌入与提取【含Matlab源码 1668期】

⛄一、SVD数字水印简介 理论知识参考文献&#xff1a;基于DWT和SVD的彩色图像数字水印算法研究 一种基于DWT-SVD的图像数字水印算法 ⛄二、部分源代码 clc close all clear %% Input images Iimread(‘Lena.jpg’); Iimresize(I,[512,512]); logorandsrc(8,8,[0,1]); figur…

青春不过,几次世界杯,足球让我明白,努力的方向

人生就像足球&#xff0c;你不会永远进球&#xff0c;但会一直跑在路上&#xff01;直到遇到足球&#xff0c;我的人生彻底改变&#xff0c;我很记得我第一个足球的样子&#xff0c;在我心里&#xff0c;它就像一颗糖果 ——梅西⚽️ 随着2022卡塔尔世界杯的开赛&#xff0c;各…

GitHub上架即巅峰,《Spring Cloud微服务架构实战》标星已超30k

有人调侃我们说&#xff1a; 程序员不如送外卖。送外卖是搬运食物&#xff0c;自己是搬运代码&#xff0c;都不产出新的东西……透支体力&#xff0c;又消耗健康&#xff0c;可替代性极强&#xff0c;30岁之后就要面临被优化的危险……想跳槽&#xff0c;但是更高的平台难进&a…

R 两组样本t检验 wilcoxon检验、卡方、fisher精确检验

一般统计方法R实现 文章目录一、读入数据二、连续性变量1.正态性和方差齐性检验2.描述统计量3.独立样本t检验和wilcoxon秩和检验三、四格表卡方或fisher精确检验&#xff08;OR【95%CI】&#xff09;1.计算各组频数和相对频率2.整理数据进行检验总结一、读入数据 library(read…

#Linux杂记# grep 查找命令常用选项大全(一)

今天&#xff0c;难得好天气&#xff0c;过去阴雨绵绵几天&#xff0c;确实干什么都没多少心劲。 对于Linux 指令&#xff0c;其实工作中用的也就那么多&#xff0c;但是真正用的很6的&#xff0c;确实给我们工作效率带来极大提高。 -i&#xff1a;在搜索的时候忽略大小写。 …

数据技术篇之离线数据开发

第4章 离线数据开发 采集系统采集的大量数据只有被整合计算后才能用于洞察商业规律&#xff0c;挖掘潜在的信息&#xff0c;实现其价值。面对海量的数据和复杂的计算&#xff0c;阿里巴巴的数据计算层包括两大体系&#xff1a; 数据存储及计算平台&#xff08;离线计算平台MaxC…

【OpenCV-Python】教程:4-8 ORB (Oriented FAST and Rotated BRIEF)

OpenCV Python ORB (Oriented FAST and Rotated BRIEF) 【目标】 理解 ORB 的基础理论 【理论】 ORB 来自于 OpenCV 实验室&#xff1b;在ICCV2011的论文 “ORB: An efficient alternative to SIFT or SURF” 提出&#xff0c;是 SIFT 和 SURF 的替代&#xff0c;没有专利问…

Java解析XML-JDK

JAXB 概述 JAXB&#xff08;Java Architecture for XML Binding&#xff09;是J2SE和J2EE平台的一部分&#xff0c;让开发者能够快速完成Java类和XML的互相映射。 JAXB 允许Java人员将Java类映射为XML表示方式。其实&#xff0c;JAXB是一个业界标准&#xff0c;是一项可以根据X…

企业景气指数、企业家信心指数-季度数据(2005-2022年)

&#xff08;2005-2022年&#xff09; 企业景气指数&#xff08;企业综合生产经营景气指数&#xff09;是根据企业负责人对本企业综合生产经营情况的判断与预期而编制的指数&#xff0c;用以综合反映企业的生产经营状况。 企业家信心指数综合反映企业家对宏观经济的看法和信心…

【Make YOLO Great Again】最终版本YOLOv1-v7全系列大解析(全网最详细汇总篇)

Rocky Ding公众号&#xff1a;WeThinkIn写在前面 【Make YOLO Great Again】栏目专注于从更实战&#xff0c;更深刻的角度解析YOLOv1-v7这个CV领域举足轻重的算法系列&#xff0c;并给出其在业务&#xff0c;竞赛以及研究维度的延伸思考。欢迎大家一起交流学习&#x1f4aa;&am…

node+vue基于微信小程序的乐团团购系统的设计与实现

随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&#xf…

Stm32旧版库函数4——nrf24l01 32位数据发送接受

stm32 32位 发射&#xff08;硬件SPI 引脚重映射&#xff09;&#xff1a; #include "stm32f10x_lib.h" #include "sys_config.h" #include "usart.h" #include "nrf.h" #include "delay.h" extern u8 TX_ADDRESS[TX_ADR_…

在Windows OS中安装与配置Scala的IDE环境

在Windows OS中安装与配置Scala的IDE环境一、Scala的安装过程二、Scala与IDE的集成一、Scala的安装过程 &#xff08;1&#xff09;下载与安装Scala软件 以下有个帖子&#xff0c;其详细记录了Window系统在安装Scala的步骤&#xff0c;可供读者参考&#xff0c; Window系统详…