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

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

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

Java并发教程–原子性和竞争条件相关推荐

  1. java 并发的原子性_Java并发教程–原子性和竞争条件

    java 并发的原子性 原子性是多线程程序中的关键概念之一. 我们说一组动作是原子的,如果它们都以不可分割的方式作为单个操作执行. 认为多线程程序中的一组操作将被串行执行是理所当然的,可能会导致错误的 ...

  2. Java并发教程– CountDownLatch

    Java中的某些并发实用程序自然会比其他并发实用程序受到更多关注,因为它们可以解决通用问题而不是更具体的问题. 我们大多数人经常遇到执行程序服务和并发集合之类的事情. 其他实用程序不太常见,因此有时它 ...

  3. Java并发教程–信号量

    这是我们将要进行的Java并发系列的第一部分. 具体来说,我们将深入探讨Java 1.5及更高版本中内置的并发工具. 我们假设您对同步和易失性关键字有基本的了解. 第一篇文章将介绍信号量-特别是对信号 ...

  4. Java并发教程–阻塞队列

    如第3部分所述,Java 1.5中引入的线程池提供了核心支持,该支持很快成为许多Java开发人员的最爱. 在内部,这些实现巧妙地利用了Java 1.5中引入的另一种并发功能-阻塞队列. 队列 首先,简 ...

  5. Java并发教程–可调用,将来

    从Java的第一个发行版开始,Java的美丽之处之一就是我们可以轻松编写多线程程序并将异步处理引入我们的设计中. Thread类和Runnable接口与Java的内存管理模型结合使用,意味着可以进行简 ...

  6. Java并发教程–重入锁

    Java的synced关键字是一个很棒的工具–它使我们能够以一种简单可靠的方式来同步对关键部分的访问,而且也不难理解. 但是有时我们需要对同步进行更多控制. 我们要么需要分别控制访问类型(读取和写入) ...

  7. Java并发教程–线程池

    Java 1.5中提供的最通用的并发增强功能之一是引入了可自定义的线程池. 这些线程池使您可以对诸如线程数,线程重用,调度和线程构造之类的东西进行大量控制. 让我们回顾一下. 首先,线程池. 让我们直 ...

  8. Java并发教程(Oracle官方资料)

    2019独角兽企业重金招聘Python工程师标准>>> 本文是Oracle官方的Java并发相关的教程,感谢并发编程网的翻译和投递. (关注ITeye官微,随时随地查看最新开发资讯. ...

  9. oracle java 并发_【转】JAVA并发教程(ORACLE官网资料)

    本文是Oracle官方的Java并发相关的教程,感谢并发编程网的翻译和投递. 计算机的使用者一直以为他们的计算机可以同时做很多事情.他们认为当其他的应用程序在下载文件,管理打印队列或者缓冲音频的时候他 ...

最新文章

  1. PyTorch一年增长194%,兼容性更强,超越TensorFlow指日可待
  2. java筑基期(10)----ajaxjson(2)
  3. PLSQL_Database Link的基本概念和用法(概念)
  4. html 自动触发 事件,js自动触发事件自定义事件
  5. notepad++ 使用去掉自动检查红线
  6. 几种关系型数据库比较
  7. cmd 日志刷新卡屏
  8. Quartz.NET的使用(附源码)
  9. 一键伪装成 Windows 10:Kali Linux 2019.4 版本推出 “Undercover” 模式
  10. Hbase table DDL操作及scala API操作
  11. yuv图片拼接 java_java利用ffmpeg把图片转成yuv格式
  12. pve万兆网卡驱动_无线环境下打游戏,还能不能更稳?附各类AX网卡换装思路
  13. windows 搭建代理服务器 - Apache httpd
  14. 全网最全的autojs例子,有一千六百多的脚本文件,少走弯路
  15. 论文中的参考文献怎么写?
  16. 语音交互的基本概念和设计实践
  17. 30个响应式大背景网页设计欣赏
  18. ArrayList扩容机制源码分析
  19. C++层Binder——Bn、Bp
  20. 《C语言》爱心代码,送给心爱之人

热门文章

  1. 2018蓝桥杯省赛---java---B---2(方格计数)
  2. zookeeper 屁民
  3. 手机钉钉在进行视频会议时怎么录屏
  4. opencv立方体的画法_用opengl立方体的画法
  5. 投票源码程序_[内附完整源码和文档] 基于JSP实现的影视创作论坛系统
  6. skimage直方图如何保存_LightGBM的参数详解以及如何调优
  7. 实现模糊查询并忽略大小写
  8. Spring Boot和Apache Camel
  9. kotlin dsl_Spring Webflux – Kotlin DSL –实现的演练
  10. spring-retry_使用Spring-Retry重试处理