Java并发教程–原子性和竞争条件

原子性是多线程程序中的关键概念之一。 我们说一组动作是原子的,如果它们都以不可分割的方式作为一个单一的操作执行。 认为多线程程序中的一组操作将被串行执行是理所当然的,可能会导致错误的结果。 原因是由于线程干扰,这意味着如果两个线程对同一数据执行多个步骤,则它们可能会重叠。

以下交织示例显示了两个线程执行多个操作(循环中的打印)以及它们如何重叠:


public class Interleaving {public void show() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " - Number: " + i);}}public static void main(String[] args) {final Interleaving main = new Interleaving();Runnable runner = new Runnable() {@Overridepublic void run() {main.show();}};new Thread(runner, "Thread 1").start();new Thread(runner, "Thread 2").start();}
}

执行时,将产生不可预测的结果。 举个例子:

Thread 2 - Number: 0
Thread 2 - Number: 1
Thread 2 - Number: 2
Thread 1 - Number: 0
Thread 1 - Number: 1
Thread 1 - Number: 2
Thread 1 - Number: 3
Thread 1 - Number: 4
Thread 2 - Number: 3
Thread 2 - Number: 4

在这种情况下,不会发生任何错误,因为它们只是打印数字。 但是,当您需要在不同步的情况下共享对象的状态(其数据)时,这会导致竞争条件的存在。

比赛条件

如果由于线程交织而有可能产生不正确的结果,则您的代码将处于竞争状态。 本节描述了两种竞争条件:

  1. 先检查后行动
  2. 读-修改-写

为了消除竞争条件并增强线程安全性,我们必须通过使用同步使这些操作成为原子操作。 以下各节中的示例将显示这些竞争条件的影响。

先行动后竞赛状态

当您有一个共享字段并希望依次执行以下步骤时,会出现此竞争条件:

  1. 从字段中获取值。
  2. 根据上一次检查的结果来做一些事情。

这里的问题是,当第一个线程在上次检查后要执行操作时,另一个线程可能已经插入并更改了字段的值。 现在,第一个线程将基于不再有效的值执行操作。 通过示例更容易看到这一点。

UnsafeCheckThenAct应该一次更改字段 。 在对changeNumber方法的调用之后,应导致执行else条件:

public class UnsafeCheckThenAct {private int number;public void changeNumber() {if (number == 0) {System.out.println(Thread.currentThread().getName() + " | Changed");number = -1;}else {System.out.println(Thread.currentThread().getName() + " | Not changed");}}public static void main(String[] args) {final UnsafeCheckThenAct checkAct = new UnsafeCheckThenAct();for (int i = 0; i < 50; i++) {new Thread(new Runnable() {@Overridepublic void run() {checkAct.changeNumber();}}, "T" + i).start();}}
}

但是由于此代码未同步,因此(无法保证)可能会导致对该字段进行多次修改:

T13 | Changed
T17 | Changed
T35 | Not changed
T10 | Changed
T48 | Not changed
T14 | Changed
T60 | Not changed
T6 | Changed
T5 | Changed
T63 | Not changed
T18 | Not changed

这种竞争条件的另一个示例是延迟初始化 。

解决此问题的一种简单方法是使用同步。

SafeCheckThenAct是线程安全的,因为它已通过同步对共享字段的所有访问来消除竞争条件。

public class SafeCheckThenAct {private int number;public synchronized void changeNumber() {if (number == 0) {System.out.println(Thread.currentThread().getName() + " | Changed");number = -1;}else {System.out.println(Thread.currentThread().getName() + " | Not changed");}}public static void main(String[] args) {final SafeCheckThenAct checkAct = new SafeCheckThenAct();for (int i = 0; i < 50; i++) {new Thread(new Runnable() {@Overridepublic void run() {checkAct.changeNumber();}}, "T" + i).start();}}
}

现在,执行此代码将始终产生相同的预期结果。 只有一个线程会更改该字段:

T0 | Changed
T54 | Not changed
T53 | Not changed
T62 | Not changed
T52 | Not changed
T51 | Not changed
...

在某些情况下,还有其他机制会比同步整个方法更好,但我不会在本文中讨论它们。

读-修改-写竞争条件

在这里,当执行以下一组操作时,会出现另一种竞争条件:

  1. 从字段中获取值。
  2. 修改值。
  3. 将新值存储到该字段。

在这种情况下,还有另一种危险的可能性,那就是丢失了对该字段的某些更新。 一种可能的结果是:

Field’s value is 1.
Thread 1 gets the value from the field (1).
Thread 1 modifies the value (5).
Thread 2 reads the value from the field (1).
Thread 2 modifies the value (7).
Thread 1 stores the value to the field (5).
Thread 2 stores the value to the field (7).

如您所见,值5的更新已丢失。

让我们看一个代码示例。 UnsafeReadModifyWrite共享一个数字字段,每次都会递增:

public class UnsafeReadModifyWrite {private int number;public void incrementNumber() {number++;}public int getNumber() {return this.number;}public static void main(String[] args) throws InterruptedException {final UnsafeReadModifyWrite rmw = new UnsafeReadModifyWrite();for (int i = 0; i < 1_000; i++) {new Thread(new Runnable() {@Overridepublic void run() {rmw.incrementNumber();}}, "T" + i).start();}Thread.sleep(6000);System.out.println("Final number (should be 1_000): " + rmw.getNumber());}
}

您能发现引起比赛状况的复合动作吗?

我敢肯定你做到了,但是为了完整起见,我还是会解释一下。 问题出在增量( number ++ )中。 这似乎是一个动作,但实际上,它是三个动作的序列(get-increment-write)。

执行此代码时,我们可能会看到丢失了一些更新:

2014-08-08 09:59:18,859|UnsafeReadModifyWrite|Final number (should be 10_000): 9996

由于无法保证线程如何交织,因此取决于您的计算机,将很难重现此更新丢失。 如果无法重现上面的示例,请尝试UnsafeReadModifyWriteWithLatch ,它使用CountDownLatch来同步线程的开始,并重复测试一百次。 您可能应该在所有结果中看到一些无效值:

Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 997
Final number (should be 1_000): 999
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000
Final number (should be 1_000): 1000

这个例子可以通过使所有三个动作原子化来解决。

SafeReadModifyWriteSynchronized在对共享字段的所有访问中使用同步:

public class SafeReadModifyWriteSynchronized {private int number;public synchronized void incrementNumber() {number++;}public synchronized int getNumber() {return this.number;}public static void main(String[] args) throws InterruptedException {final SafeReadModifyWriteSynchronized rmw = new SafeReadModifyWriteSynchronized();for (int i = 0; i < 1_000; i++) {new Thread(new Runnable() {@Overridepublic void run() {rmw.incrementNumber();}}, "T" + i).start();}Thread.sleep(4000);System.out.println("Final number (should be 1_000): " + rmw.getNumber());}
}

让我们看另一个删除此竞争条件的示例。 在这种特定情况下,由于字段号与其他变量无关,因此我们可以使用原子变量。

SafeReadModifyWriteAtomic使用原子变量来存储字段的值:

public class SafeReadModifyWriteAtomic {private final AtomicInteger number = new AtomicInteger();public void incrementNumber() {number.getAndIncrement();}public int getNumber() {return this.number.get();}public static void main(String[] args) throws InterruptedException {final SafeReadModifyWriteAtomic rmw = new SafeReadModifyWriteAtomic();for (int i = 0; i < 1_000; i++) {new Thread(new Runnable() {@Overridepublic void run() {rmw.incrementNumber();}}, "T" + i).start();}Thread.sleep(4000);System.out.println("Final number (should be 1_000): " + rmw.getNumber());}
}

以下帖子将进一步说明机制,如锁定或原子变量。

结论

这篇文章解释了在非同步多线程程序中执行复合操作时隐含的一些风险。 为了强制执行原子性并防止线程交织,必须使用某种类型的同步。

  • 您可以在github上查看源代码。

翻译自: https://www.javacodegeeks.com/2014/08/java-concurrency-tutorial-atomicity-and-race-conditions.html

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

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

相关文章

sqlite3 数据库(一)

SQLite 数据库&#xff0c;是一个非常轻量级自包含(lightweight and self-contained)的DBMS&#xff0c;它可移植性好&#xff0c;很容易使用&#xff0c;很小&#xff0c;高效而且可靠。 SQLite嵌入到使用它的应用程序中&#xff0c;它们共用相同的进程空间&#xff0c;而不是…

HTML引入vue.js,在ie浏览器中不显示

因为只有两个页面&#xff0c;所以我没有用 vue-cli 搭框架&#xff0c;直接在 HTML 中引入vue.js 文件。发现其他浏览器都能正常显示&#xff0c;ie 下显示不正常&#xff0c;而且还报错&#xff0c;错误信息如下&#xff1a; 原因&#xff1a; 主要是因为 ie 不支持 ES6 的语…

【button】 按钮组件说明

原型&#xff1a; <buttonsize"[default | mini]"type"[primary | default | warn]"plain"[Boolean]"disabled"[Boolean]"loading"[Boolean]"form-type"[submit | reset]"open-type"[contact | share | g…

具有Infinispan的聚集幂等消费者模式

我创建了一个小项目 &#xff0c;该项目展示了如何将JBoss Infinispan与Apache Camel和幂等消费者模式一起使用&#xff0c;以确保消息不会在集群环境中被处理两次。 假设您有一个应用程序&#xff0c;该应用程序必须通过将其部署在多个容器上才能轻松扩展。 但是应用程序必须…

UVa OJ 128 - Software CRC (软件CRC)

Time limit: 3.000 seconds限时&#xff1a;3.000秒 Problem问题 You work for a company which uses lots of personal computers. Your boss, Dr Penny Pincher, has wanted to link the computers together for some time but has been unwilling to spend any money on the…

ipv4编址

IPv4的编址&#xff1a; IPv4的地址有32位&#xff0c;通过使用点分十进制法&#xff0c;将其划分成4个由“.”隔断的部分&#xff0c;每一个部分的取值是0~255 {2^0~(2^8)-1} IP地址是32位类似这样的二进制串&#xff1a;1100 0000 1111 1111 1111 1111 1111 1110&#xff08;…

基于 vue 的验证码组件

登录页面有个验证码&#xff0c;暂时没用到后台&#xff0c;在网上找了两个博客&#xff0c;记录一下。 一、直接写&#xff08;参考-UIEngineer&#xff09; 这个样式比较简单&#xff0c;直接在需要验证码的地方添加就行了。如果这个页面比较复杂&#xff0c;用组件会比较好…

Java 8 Friday:更多功能关系转换

过去&#xff0c;我们一直在每个星期五为您提供有关Java 8的新内容的新文章。这是一个非常令人兴奋的博客系列 &#xff0c;但我们想再次将重点放在Java和SQL的核心内容上。 我们仍然偶尔会写关于Java 8的博客&#xff0c;但不再是每个星期五&#xff08;有些人已经注意到&…

【WXS全局对象】Date

属性&#xff1a; 名称说明Date.parse( [dateString] )解析一个日期时间字符串&#xff0c;并返回 1970/1/1 午夜距离该日期时间的毫秒数。Date.UTC(year,month,day,hours,minutes,seconds,ms) 根据世界时返回 1970 年 1 月 1 日 到指定日期的毫秒数。 参数&#xff1a;year/m…

13个不可不知的ASP.NET MVC扩展点

ASP.NET MVC设计的主要原则之一是可扩展性。处理管线&#xff08;processing pipeline&#xff09;上的所有&#xff08;或大多数&#xff09;东西都是可替换的。因此&#xff0c;如果您不喜欢ASP.NET MVC所使用的约定&#xff08;或缺乏某些约定&#xff09;&#xff0c;您可以…

程序员常用的3大Web安全漏洞防御解决方案:XSS、CSRF及SQL注入(图文详解)

https://blog.csdn.net/ChenRui_yz/article/details/86489067 随着互联网的普及&#xff0c;网络安全变得越来越重要&#xff0c;程序员需要掌握最基本的web安全防范&#xff0c;下面列举一些常见的安全漏洞和对应的防御措施。01 常见的Web安全问题1.前端安全XSS 漏洞CSRF 漏洞…

在 HTML 中引入 vue.js 写页面

突然说要写两个页面&#xff08;只有两个页面&#xff0c;不是一个完整的项目。。&#xff09;&#xff0c;有点懵&#xff0c;不知道从哪下手&#xff0c;而且只对 vue 熟悉那么一丢丢&#xff0c;其他框架不是很熟悉。但是没办法鸭&#xff0c;只能硬着头皮去做了&#xff01…

JavaFX技巧14:StackPane子项-隐藏但不消失

另一个简短提示&#xff1a;Swing提供了一个名为CardLayout的布局管理器&#xff0c;该管理器管理容器内的一组组件&#xff08;卡&#xff09;&#xff0c;但始终仅显示其中一个。 方法CardLayout.show&#xff08;Container &#xff0c;String&#xff09;允许在组件/卡之间…

【WXS数据类型】Array

属性&#xff1a; 名称值类型说明[Array].constructor[String]返回值为“Array”,表示类型的结构字符串[Array].length[Number]返回数组长度 方法&#xff1a; 原型&#xff1a;[Array].toString() 说明&#xff1a;将数组转换成字符串&#xff0c;用逗号分隔每个元素 原型&am…

Mschart图表制作

首先一次安装这三个 &#xff08;1&#xff09;.Microsoft .NET Framework 3.5 的 Microsoft 图表控件 &#xff08;2&#xff09;.Microsoft .NET Framework 3.5 语言包的 Microsoft 图表控件 &#xff08;3&#xff09;.Microsoft Chart Controls Add-on for Microsoft Visua…

vue打包后,font格式错误

本地测试没有问题&#xff0c;项目打包以后&#xff0c;浏览器打开控制台&#xff0c;提示font格式错误&#xff1a; 把我的双引号给去掉了。。。-^- 不开心。 解决办法&#xff1a; 1. 把 font: 字体粗细 字体大小/行高 "字体样式"; 分开来写。 改成&#xff1a;…

获取可用密码算法的列表

您如何学习可用的密码算法&#xff1f; Java规范列出了几种必需的密码&#xff0c;摘要等&#xff0c;但是提供程序通常提供的不止这些。 幸运的是&#xff0c;这很容易了解我们系统上的可用内容。 public class ListAlgorithms {public static void main(String[] args) {//…

【...】小程序扩展运算符 ... 说明

小程序扩展运算符 ... &#xff0c;用来将一个对象展开。 以下示例&#xff1a; 1 <template is"objectCombine" data"{{...obj1, ...obj2, e: 5}}"></template> 1 Page({2 data: {3 obj1: {4 a: 1,5 b: 26 },7 o…

fhq_treap || BZOJ 3224: Tyvj 1728 普通平衡树 || Luogu P3369 【模板】普通平衡树

题面&#xff1a;【模板】普通平衡树 代码&#xff1a; 1 #include<cstdio>2 #include<cstring>3 #include<iostream>4 #include<cstdlib>5 using namespace std;6 inline int rd(){7 int x0,f1;char cgetchar();8 while(c<0||c>9){if(c…

关于Zend framework 里一段代码的疑问

初学框架 看了一些代码 有一些疑问 在此记录 1 publicfunction__isset($key)2 {3 returnthis->_engine->get_temlate_vars($key)!null;4 }我想这个函数返回的一定是一个bool值 但不知道get_temlate_vars($key)!null 这里面的!是怎么个用法 希望有知道的能告诉我一声 谢谢…