《Java线程与并发编程实践》—— 第2章 同步 2.1 线程中的问题

简介: Java线程与并发编程实践 线程交互通常是通过共享变量完成的,当线程之间没有交互时,开发多线程的应用程序会变得简单许多。一旦发生了交互,很多诱发线程不安全(在多线程环境下不正确)的因素就会暴露出来。在这一章中,你将会认识到这些问题,同时也会学习如何正确地使用Java面向同步的特性来克服它们。

本节书摘来异步社区《Java线程与并发编程实践》一书中的第2章,第2.1节,作者: 【美】Jeff Friesen,更多章节内容可以访问云栖社区“异步社区”公众号查看。

第2章 同步

Java线程与并发编程实践
线程交互通常是通过共享变量完成的,当线程之间没有交互时,开发多线程的应用程序会变得简单许多。一旦发生了交互,很多诱发线程不安全(在多线程环境下不正确)的因素就会暴露出来。在这一章中,你将会认识到这些问题,同时也会学习如何正确地使用Java面向同步的特性来克服它们。

2.1 线程中的问题

Java对线程的支持促进了响应式、可扩展应用程序的发展。不过,这样的支持是以增加复杂性作为代价的。如果不多加小心,你的代码就会到处充斥着极难以察觉的bug,而这些bug多和竞态条件、数据竞争以及缓存变量有关。

2.1.1 竞态条件

当计算的正确性取决于相对时间或者调度器所控制的多线程交叉时,竞态条件就会发生。下面的代码片段描述了只要满足一个特定的前置条件,就会触发计算的场景:
``
if (a == 10.0)
b = a / 2.0;
``
在单线程的环境中,这段程序没有任何问题。在多线程环境下,如果a和b都是局部变量,那么也没有问题。 但是, 假设a和b是实例变量或者类(static)变量,并且有两条线程同时访问这段代码,就有问题了。

假设一条线程已经执行完if (a == 10.0),在即将执行b = a / 2.0时,被调度器暂停了,与此同时,调度器恢复了另一条线程改变了a的值;当前一条线程恢复执行,变量b却不会等于5.0(如果a和b是局部变量,因为每个线程都会有自己的局部变量拷贝,所以竞态条件不会发生)。

这段代码就是竞态条件中称为check-then-act的一个经典例子。在这种竞态条件下,很可能会用过时的观测状态来决定下一步的动作。在前面的代码片段中,“检查”是if (a == 10.0),“动作”则是b = a / 2.0;。

另外一种类型的竞态条件就是read-modify-write,这种情况下,新状态继承自旧状态。旧状态被读取,然后更改,最后更新,通过这3个不可分割的操作来得到更改后的结果。只不过,这些操作的组合并非不可分割。

典型的read-modify-write的例子就是用一个递增的变量来生成唯一的数字标识。在下面的代码片段中,假设counter变量是一个类型为int(初始化为1)的实例变量,两条线程同时访问这段代码:

public int getID()
{
   return counter++;
}

尽管看上去这是个单一操作,但事实上,表达式counter++是3个单独的操作:读取counter的值,给值加1,然后把更新之后的值存储到counter中。当时读取的值就是整个表达式的返回值。

假设在被调度器阻断之前,线程1调用了getID()方法,同时读取了counter的值,此时其值是1。现在,假设线程2运行,调用了getID()方法,读取了counter的值(1),对这个值加1,把结果(2)存储到counter中,然后将1返回给调用者。

在这种情况下,假设线程1恢复过来了,对之前读到的值(1)加1,然后把结果(2)存储到counter变量中,然后将1返回给调用者。由于线程1撤销了线程2的动作,我们就会错过一次递增并生成了一个重复的ID。所以这个方法是无效的。

2.1.2 数据竞争

竞态条件经常会和数据竞争相混淆。数据竞争指的是两条或两条以上的线程(在单个应用中)并发地访问同一块内存区域,同时其中至少有一条是为了写,而且这些线程没有协调对那块内存区域的访问。当满足这些条件的时候,访问顺序就是不确定的。依据这种顺序,每次运行都可能会产生不同的结果。看下面的例子:

private static Parser parser;

public static Parser getInstance()
{
   if (parser == null)
      parser = new Parser();
   return parser;
}

假设线程1首先调用了getInstance()方法。由于它检测到属性parser是空值,线程1就会实例化Parser并且将引用赋给变量parser。随后,当线程2调用getInstance()方法时,它可能检测到parser已经包含了一个非空的引用,于是简单地返回了parser的值;另一种可能是,线程2检测到parser的值仍然是空,于是创建了一个新的Parser的对象。由于线程1写parser变量和线程2读parser变量之间没有happens-before ordering(一个动作先于另一个动作发生)的保证(这里不存在对parser访问顺序的协同),数据竞争产生了。

2.1.3 缓存变量

为了提升性能,编译器Java虚拟机(JVM)以及操作系统会协调在寄存器中或者处理器缓存中缓存变量,而不是依赖主存。每条线程都会有其自己的变量拷贝。当线程写入这个变量的时候,其实是写入自己的拷贝;其他线程不太可能在看到自己的变量拷贝发生更改。

第1章给出的ThreadDemo的应用程序(参见清单1-3)暴露了这个问题。这里我重新列出部分源码以供参考:

private static BigDecimal result;

public static void main(String[] args)
{
   Runnable r = () ->
                {
                   result = computePi(50000);
                };
   Thread t = new Thread(r);
   t.start();
   try
   {
      t.join(); 
   }
   catch (InterruptedException ie)
   {
      // Should never arrive here because interrupt() is never
      // called. 
    }
   System.out.println(result);
}

类属性result示范了缓存变量的问题。该属性在lambda表达式的上下文当中被一条工作线程访问并执行代码result = computePi(50000);,然后默认主线程执行System.out.println(result);

这个工作线程能够将computePi()的返回值存储到自己的result变量的拷贝中。默认主线程很可能无法看到result = computePi(50000);的赋值,并且它的本地拷贝会保持原来默认的null值。这个null值会取代result的字符串表示(即计算好的pi的值)被打印出来。

相关文章
|
8天前
|
算法 Java 开发者
Java中的多线程编程:概念、实现与性能优化
【4月更文挑战第9天】在Java编程中,多线程是一种强大的工具,它允许开发者创建并发执行的程序,提高系统的响应性和吞吐量。本文将深入探讨Java多线程的核心概念,包括线程的生命周期、线程同步机制以及线程池的使用。接着,我们将展示如何通过继承Thread类和实现Runnable接口来创建线程,并讨论各自的优缺点。此外,文章还将介绍高级主题,如死锁的预防、避免和检测,以及如何使用并发集合和原子变量来提高多线程程序的性能和安全性。最后,我们将提供一些实用的性能优化技巧,帮助开发者编写出更高效、更稳定的多线程应用程序。
|
6天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
1天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
14 0
|
2天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。
|
4天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
4天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
11 4
|
5天前
|
Java
探秘jstack:解决Java应用线程问题的利器
探秘jstack:解决Java应用线程问题的利器
14 1
探秘jstack:解决Java应用线程问题的利器
|
5天前
|
Java 调度 开发者
Java 21时代的标志:虚拟线程带来的并发编程新境界
Java 21时代的标志:虚拟线程带来的并发编程新境界
14 0
|
8天前
|
监控 安全 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第10天】 在Java开发中,并发编程是提升应用性能和响应能力的关键手段。然而,线程安全问题和性能调优常常成为开发者面临的挑战。本文将通过分析Java并发模型的核心原理,探讨如何平衡线程安全与系统性能。我们将介绍关键的同步机制,包括synchronized关键字、显式锁(Lock)以及并发集合等,并讨论它们在不同场景下的优势与局限。同时,文章将提供实用的代码示例和性能测试方法,帮助开发者在保证线程安全的前提下,实现高效的并发处理。
|
8天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信