Java并发的问题及应对办法

简介: 并发问题的源头并发?为啥需要并发呢?自然是为了性能,增强算力以及协调能力在现今计算机器体系中,涉及性能的主要有CPU、内存、IO三方面,而这三者的速度也是天壤之别,形象之讲,CPU天上一天,内存是地上一年,IO则要地上十年

并发问题的源头

并发?为啥需要并发呢?自然是为了性能,增强算力以及协调能力

在现今计算机器体系中,涉及性能的主要有CPU、内存、IO三方面,而这三者的速度也是天壤之别,形象之讲,CPU天上一天,内存是地上一年,IO则要地上十年

怎么应对:

1、CPU增加了多级缓存,均衡与内存的速度差异,并且还从单核发展为多核增加算力

2、操作系统增加线程,分时复用CPU,均衡CPU与IO的速度差异

3、通过即时编译器重排序,处理器乱序执行,以及内存系统重排序优化指令执行次序,更好地利用缓存

但这些措施并不是百利无害的,并发问题就是其中一害。

1、缓存导致的可见性问题

多核时代,每个核都有各自的L1,L2缓存,在各自缓存中修改的数据相互不可见。

image.png

《缓存是个面子工程》[1]提到的硬件缓存,也带来了并发问题。

image.png

2、线程切换带来的原子性问题

这主要有些看似一行的代码,其实需要多条CPU指令才能完成

如count+=1,需要三条指令

指令1:把变量count从内存加载到CPU的寄存器

指令2:在寄存器中执行+1操作

指令3:最后将结果写入内存

当多线程时,线程切换时三条指令就会被错误执行,打破了原子性,导致逻辑的错误。

3、编译优化带来的有序性问题

编译器为了优化性能,有时改变了程序中语句的先后顺序。

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

new Singleton()这句话感觉是

1.分配一块内存M2.在内存M上初始化Singleton对象3.然后M的地址赋值给instance变量

实际执行路径却是:

1.分配一块内存M2.将M的地址赋值给instance变量3.最后在内存M上初始化Singleton对象

JMM

如何解决上述的三大问题,JSR-133定义了内存模型JMM

A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program. For the Java programming language, the memory model works by examining each read in an execution trace and checking that the write observed by that read is valid according to certain rules.

也就是说一个内存模型描述了一个给定的程序和和它的执行路径是否一个合法的执行路径。对于java序言来说,内存模型通过考察在程序执行路径中每一个读操作,根据特定的规则,检查写操作对应的读操作是否能是有效的。java内存模型只是定义了一个规范,具体的实现可以是根据实际情况自由实现的。但是实现要满足java内存模型定义的规范。

内存模型的种类大致有两种:

Sequential Consistency Memory Model: 连续一致性模型。这个模型定义了程序执行的顺序和代码执行的顺序是一致的。也就是说 如果两个线程,一个线程T1对共享变量A进行写操作,另外一个线程T2对A进行读操作。如果线程T1在时间上先于T2执行,那么T2就可以看见T1修改之后的值。这个内存模型比较简单,也比较直观,比较符合现实世界的逻辑。但是这个模型定义比较严格,在多处理器并发执行程序的时候,会严重的影响程序的性能。因为每次对共享变量的修改都要立刻同步会主内存,不能把变量保存到处理器寄存器里面或者处理器缓存里面。导致频繁的读写内存影响性能。

这种模型相当于禁用了缓存。如果再禁止编译器优化,就算是彻底解决上述问题了,但性能将受到严重影响。

Happens-Before Memory Model: 先行发生模型。这个模型理解起来就比较困难。先介绍一个先行发生关系 (Happens-Before Relationship)   如果有两个操作A和B存在A Happens-Before B,那么操作A对变量的修改对操作B来说是可见的这个先行并不是代码执行时间上的先后关系,而是保证执行结果是顺序的

happens-before

happens-before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 happens-before 规则。

happens-before规则:

程序次序规则(program order rule): 在一个线程内,先在前面的代码操作先行。准确的说控制流顺序而不是代码顺序。需要考虑分支,循环等结构。

管程锁定规则(monitor lock rule):同一个资源锁,先unlock,之后才能lock。

Volatile变量规则(volatile variable rule):一个变量被volatile修饰,多线程操作,先执行操作,再执行读操作。(同时写操作只能有一个)

线程启动规则(Thread start rule):Thread对象的start方法,先行发生于此线程的每一个方法。

线程终止规则(Thread Termination rule):该线程的所有方法,先行发生于该线程的终止检测方法。例如:可以通过Thread.join方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行。

线程中断规则(Thread Interruption Rule): 中断方法先行发生于,中断检测方法。中断方法interrupt(),中断检测interrupted()方法。

对象终结规则(finalizer rule): 一个对象的初始化完成(构造函数执行结束)先行发生于它的finalizer方法的开始。

传递性(Transitivity): 如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

as-if-serial

在谈happens-befre常会提到as-if-serial

即时编译器保证程序能够遵守as-if-serial属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。也就是经过重排序的执行结果要与顺序执行的结果保持一致。

而且,如果两个操作之间存在数据依赖时,编译器不能调整它们的顺序,否则将造成程序语义的改变。

public class AsIfSerialDemo {
    public static void main(String[] args) {
        int a = 10;//1
        int b = 10;//2
        int c = a+ b;//3
    }
}

上面示例中:1和3之间存在数据依赖关系,同时2和3之间也存在数据依赖关系。因此在最终执行的指令序列中,3不能被重排序到1和2的前面(3排到1和2的前面,程序的结果将会被改变)。但1和2之间没有数据依赖关系,编译器和处理器可以重排序1和2之间的执行顺序。

VS 时间先行

对于happens-before先行发生,怎么理解,最常与“时间先后发生”搞混淆。

happens-before 关系是用来描述两个操作的内存可见性的。

如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。那么与“时间先后发生”顺序有什么区别?

《JSR-133: JavaTM Memory Model and Thread Specification》[2],happens-before是这样定义的:

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second. It should be stressed that a happens-before relationship between two actions does not imply that those actions must occur in that order in a Java platform implementation. The happens-before relation mostly stresses orderings between two actions that conflict with each other, and defines when data races take place.

从定义中可以看出两点:

1、the first is visible to and ordered before the second

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前

2、does not imply that those actions must occur in that order in a Java platform implementation

两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行


由这两条可以得出,JMM是要求当有happens-before关系时,不仅要求了可见性,而且在时间上也得保证有序。然而在不改变语义的前提下,Java平台的实现可以自主决定。这也就表明了happens-before与时间先后没有更大的关联性。

A happens-before B does not imply A happening before B.

A happening before B does not imply A happens-before B.

一个操作 “先行发生” 并不意味着这个操作必定是“时间上的先发生”


// 以下操作在同一个线程中执行
int i = 1;
int j = 2;

根据happens-before规则第一条,“int i = 1” 的操作先行发生(Happens-before)于 “int j = 2”,但在保证语义不改变的前提下,重排序了两条语句,那在时间上,“int j=2”先执行了。

《The Happens-Before Relation》[3]这篇文章中,作者还举了个示例:


int A = 0;
int B = 0;
void foo()
{
    A = B + 1;              // (1)
    B = 1;                  // (2)
}

虽然(1) happens-before (2),而且从上面的as-if-serial判断,(1) 得happen before (2) ,但作者观察并不是。

image.gifimage.png

从图上可看出,A被赋值为0,B被赋值为1,但 (1) 没被执行呢。

关于这个问题,在stackoverflow happens-before [4] 被讨论了。有人指出作者说得不对,而也有人给出解答:

A and B are locations in memory. However the operation B+1 does not happen in memory, it happens in the CPU. Specifically, the author is describing these two operations.

A = B + 1 (1)

A1 - The value in memory location B (0) is loaded into a CPU registerA2 - The CPU register is incremented by 1A3 - The value in the CPU register (1) is written to memory location A

B = 1 (2)

B1 - The value 1 is written to memory location B

Happens-Before requires that the read of B (step A1) happens before the write of B (step B1). However, the rest of the operations have no interdependence and can be reordered without affecting the result. Any of these sequences will produce the same outcome

A1, B1, A2, A3A1, A2, B1, A3A1, A2, A3, B1

一个操作 “时间上的先发生” 也不能代表这个操作会是“先行发生”

private int value = 0;
// 线程 A 调用
pubilc void setValue(int value){    
    this.value = value;
}
// 线程 B 调用
public int getValue(){
    return value;
}

假设存在线程 A 和 B,线程 A 先(时间上的先后)调用了 setValue(1),然后线程 B 调用了同一个对象的 getValue() ,那么线程 B 收到的返回值是什么? 0和1都有可能。因为两个操作之间没有happens-before关系。

volatile

volatile字段的happens-before关系指的是在两个不同线程中,【volatile的写操作】 happens-before之后 【对同一字段的读操作】。这里有个关键字“之后”,指的是时间上的先后。也就是我这边写,你之后再读就一定能读得到我刚刚写的值。普通字段则没有这个保证。也就是上面的setValue()与getValue()示例问题


int a=0;
volatile int b=0;
public void method1() {
  int r2 = a;
  b = 1;
}
public void method2() {
  int r1 = b;
  a = 2;
}

首先,b加了volatile之后,并不能保证b=1一定先于r1=b,而是保证r1=b始终能够看到b的最新值。比如说b=1;b=2,之后在另一个CPU上执行r1=b,那么r1会被赋值为2。如果先执行r1=b,然后在另外一个CPU上执行b=1和b=2,那么r1将看到b=1之前的值。

在没有标记volatile的时候,同一线程中,r2=a和b=1存在happens before关系,但因为没有数据依赖可以重排列。一旦标记了volatile,即时编译器和CPU需要考虑到多线程happens-before关系,因此不能自由地重排序。

volatile与synchronized的区别,可以查看《volatile synchronized cas》[5]

总结

本篇总结了Java并发问题的本质:可见性、原子性、有序性;以及应对这些问题,JMM中happens-before模型的规则。以及happens-before与happen before的区别。

References

[1] 《缓存是个面子工程》: https://www.zhuxingsheng.com/blog/caching-is-a-face-project.html

[2] 《JSR-133: JavaTM Memory Model and Thread Specification》: https://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf

[3] 《The Happens-Before Relation》: https://preshing.com/20130702/the-happens-before-relation/

[4] stackoverflow happens-before : https://stackoverflow.com/questions/53264829/i-know-that-happens-before-does-not-imply-happening-before-can-the-code-a-b

[5] 《volatile synchronized cas》: https://www.zhuxingsheng.com/blog/volatile-synchronized--cas.html

目录
相关文章
|
6月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
127 2
|
6月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
92 1
|
3月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
3月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
3月前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
89 2
|
4月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
54 1
|
5月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
6月前
|
存储 Java
Java 中 ConcurrentHashMap 的并发级别
【8月更文挑战第22天】
84 5
|
6月前
|
存储 算法 Java
Java 中的同步集合和并发集合
【8月更文挑战第22天】
62 5
|
6月前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
86 2

热门文章

最新文章