【高并发】解密导致并发问题的第二个幕后黑手——原子性问题

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 原子性是指一个或者多个操作在CPU中执行的过程不被中断的特性。原子性操作一旦开始运行,就会一直到运行结束为止,中间不会有中断的情况发生。

大家好,我是冰河~~

今天,我们继续大冰和小菜的故事。

写在前面

大冰:小菜童鞋,昨天讲解的内容复习了吗?

小菜:复习了大冰哥,昨天的内容干货满满啊,感觉自己收获很大。

大冰:那你说说昨天都讲了哪些内容呢?

小菜:昨天主要讲了线程的可见性和可见性问题。可见性是指一个线程对共享变量的修改,另一个线程能够立刻看到,如果不能立刻看到,就可能会产生可见性问题。在单核CPU上是不存在可见性问题的,可见性问题主要存在于运行在多核CPU上的并发程序。归根结底,可见性问题还是由CPU的缓存导致的,而缓存导致的可见性问题是导致诸多诡异的并发编程问题的“幕后黑手”之一。

大冰:很好,小菜童鞋,复习的不错,今天,我们继续讲并发问题的第二个“幕后黑手”——线程切换带来的原子性问题,这个知识点也是非常重要的,一定要好好听。

原子性

原子性是指一个或者多个操作在CPU中执行的过程不被中断的特性。原子性操作一旦开始运行,就会一直到运行结束为止,中间不会有中断的情况发生。

我们也可以这样理解原子性,就是线程在执行一系列操作时,这些操作会被当做一个不可拆分的整体执行,这些操作要么全部执行,要么全部不执行,不会存在只执行一部分的情况,这就是原子性操作。

关于原子性操作一个典型的场景就是转账,例如,小明和小刚的账户余额都是200元,此时小明给小刚转账100元,如果转账成功,则小明的账户余额为100元,小刚的账户余额为300元;如果转账失败,则小明和小刚的账户余额仍然为200元。不会存在小明账户为100元,小刚账户为200元,或者小明账户为200元,小刚账户为300元的情况。

这里,小明给小刚转账100元的操作,就是一个原子性操作,它涉及小明账户余额减少100元,小刚账户余额增加100元的操作,这两个操作是一个不可分割的整体,要么全部执行,要么全部不执行。

小明给小刚转账成功,则如下所示。

小明给小刚转账失败,则如下所示。

不会出现小明账户为100元,小刚账户为200元的情况。

也不会出现小明账户为200元,小刚账户为300元的情况。

线程切换

在并发编程中,往往设置的线程数目会大于CPU数目,而每个CPU在同一时刻只能被一个线程使用。而CPU资源的分配采用了时间片轮转策略,也就是给每个线程分配一个时间片,线程在这个时间片内占用CPU的资源来执行任务。当占用CPU资源的线程执行完任务后,会让出CPU的资源供其他线程运行,这就是任务切换,也叫做线程切换或者线程的上下文切换。

如果大家还是不太理解的话,我们可以用下面的图来模拟线程在CPU中的切换过程。

在图中存在线程A和线程B两个线程,其中线程A和线程B中的每个小方块代表此时线程占有CPU资源并执行任务,这个小方块占有的时间,被称为时间片,在这个时间片中,占有CPU资源的线程会在CPU上执行,未占有CPU资源的线程则不会在CPU上执行。而每个虚线部分就代表了此时的线程不占用CPU资源。CPU会在线程A和线程B之间频繁切换。

原子性问题

理解了什么是原子性,再看什么是原子性问题就比较简单了。

原子性问题是指一个或者多个操作在CPU中执行的过程中出现了被中断的情况。

线程在执行某项操作时,此时如果CPU发生了线程切换,CPU转而去执行其他的任务,中断了当前线程执行的操作,这就会造成原子性问题。

如果你还不能理解的话,我们来举一个例子:假设你在银行排队办理业务,小明在你前面,柜台的业务员为小明办理完业务,正好排到你时,此时银行下班了,柜台的业务员微笑着告诉你:实在不好意思,先生(女士),我们下班了,您明天再来吧!此时的你就好比是正好占有了CPU资源的线程,而柜台的业务员就是那颗发生了线程切换的CPU,她将线程切换到了下班这个线程,执行下班的操作去了。

Java中的原子性问题

在Java中,并发程序是基于多线程技术来编写的,这也会涉及到CPU的对于线程的切换问题,正是CPU中对任务的切换机制,导致了并发编程会出现原子性的诡异问题,而原子性问题,也成为了导致并发问题的第二个“幕后黑手”。

在并发编程中,往往Java语言中一条简单的语句,会对应着CPU中的多条指令,假设我们编写的ThreadTest类的代码如下所示。

package io.mykit.concurrent.lab01;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试原子性
 */
public class ThreadTest {

    private Long count;

    public Long getCount(){
        return count;
    }

    public void incrementCount(){
        count++;
    }
}

接下来,我们打开ThreadTest类的class文件所在的目录,在cmd命令行输入如下命令。

javap -c ThreadTest

得出如下的结果信息,如下所示。

d:>javap -c ThreadTest
Compiled from "ThreadTest.java"
public class io.mykit.concurrent.lab01.ThreadTest {
  public io.mykit.concurrent.lab01.ThreadTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public java.lang.Long getCount();
    Code:
       0: aload_0
       1: getfield      #2                  // Field count:Ljava/lang/Long;
       4: areturn

  public void incrementCount();
    Code:
       0: aload_0
       1: getfield      #2                  // Field count:Ljava/lang/Long;
       4: astore_1
       5: aload_0
       6: aload_0
       7: getfield      #2                  // Field count:Ljava/lang/Long;
      10: invokevirtual #3                  // Method java/lang/Long.longValue:()J
      13: lconst_1
      14: ladd
      15: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
      18: dup_x1
      19: putfield      #2                  // Field count:Ljava/lang/Long;
      22: astore_2
      23: aload_1
      24: pop
      25: return
}

这里,我们主要关注下incrementCount()方法对应的CPU指令,如下所示。

public void incrementCount();
    Code:
       0: aload_0
       1: getfield      #2                  // Field count:Ljava/lang/Long;
       4: astore_1
       5: aload_0
       6: aload_0
       7: getfield      #2                  // Field count:Ljava/lang/Long;
      10: invokevirtual #3                  // Method java/lang/Long.longValue:()J
      13: lconst_1
      14: ladd
      15: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
      18: dup_x1
      19: putfield      #2                  // Field count:Ljava/lang/Long;
      22: astore_2
      23: aload_1
      24: pop
      25: return

可以看到,Java语言中短短的几行incrementCount()方法竟然对应着那么多的CPU指令。这些CPU指令我们大致可以分成三步。

  • 指令1:把变量count从内存加载的CPU寄存器。
  • 指令2:在寄存器中执行count++操作。
  • 指令3:将结果写入缓存(可能是CPU缓存,也可能是内存)。

在操作系统执行线程切换时,可能发生在任何一条CPU指令完成后,而不是程序中的某条语句完成后。如果线程A执行完指令1后,操作系统发生了线程切换,当两个线程都执行count++操作后,得到的结果是1而不是2。这里,我们可以使用下图来表示这个过程。

由上图,我们可以看出:线程A将count=0加载到CPU的寄存器后,发生了线程切换。此时内存中的count值仍然为0,线程B将count=0加载到寄存器,执行count++操作,并将count=1写到内存。此时,CPU切换到线程A,执行线程A中的count++操作后,线程A中的count值为1,线程A将count=1写入内存,此时内存中的count值最终为1。

所以,如果在CPU中存在正在执行的线程,恰好此时CPU发生了线程切换,则可能会导致原子性问题,这也是导致并发编程频繁出问题的根源之一。我们只有充分理解并掌握线程的原子性以及引起原子性问题的根源,并在日常工作中时刻注意编写的并发程序是否存在原子性问题,才能更好的编写出并发程序。

总结

缓存带来的可见性问题、线程切换带来的原子性问题和编译优化带来的有序性问题,是导致并发编程频繁出现诡异问题的三个源头,我们已经介绍了缓存带来的可见性问题和线程切换带来的原子性问题。下一篇中,我们继续深耕高并发中的有序性问题。

写在最后

大冰:好了,今天就是我们讲的主要内容了,今天的内容同样最重要,回去后要好好复习。

小菜:好的,大冰哥,一定好好复习。

文末福利

最后,附上并发编程需要掌握的核心技能知识图,祝大家在学习并发编程时,少走弯路。

推荐阅读

要想学好并发编程,关键是要理解这三个核心问题

实践出真知:全网最强秒杀系统架构解密,不是所有的秒杀都是秒杀!!

注意:线程的执行顺序与你想的并不一样!!

深入解析Callable接口

两种异步模型与深度解析Future接口!

解密SimpleDateFormat类的线程安全问题和六种解决方案!

不得不说的线程池与ThreadPoolExecutor类浅析

深度解析线程池中那些重要的顶层接口和抽象类

从源码角度分析创建线程池究竟有哪些方式

通过源码深度解析ThreadPoolExecutor类是如何保证线程池正确运行的

通过ThreadPoolExecutor类的源码深度解析线程池执行任务的核心流程

通过源码深度分析线程池中Worker线程的执行流程

从源码角度深度解析线程池是如何实现优雅退出的

ScheduledThreadPoolExecutor与Timer的区别和简单示例

深度解析ScheduledThreadPoolExecutor类的源代码

由InterruptedException异常引发的思考

浅谈AQS中的CountDownLatch、Semaphore与CyclicBarrier

浅谈AQS中的ReentrantLock、ReentrantReadWriteLock、StampedLock与Condition

朋友去面试竟然栽在了Thread类的源码上

如何使用Java7提供的Fork/Join框架实现高并发程序?

一文解密诡异并发问题的第一个幕后黑手——可见性问题

好了,今天就到这儿吧,我是冰河,我们下期见~~

目录
相关文章
|
数据采集 并行计算 Java
【文末送书】Python高并发编程:探索异步IO和多线程并发
【文末送书】Python高并发编程:探索异步IO和多线程并发
260 0
|
5月前
|
监控 应用服务中间件 nginx
高并发架构设计三大利器:缓存、限流和降级问题之Nginx的并发连接数计数的问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之Nginx的并发连接数计数的问题如何解决
|
5月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
74 0
|
5月前
|
设计模式 存储 缓存
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
64 0
|
7月前
|
缓存 架构师 算法
高并发系统简单玩!Alibaba全新出品亿级并发设计速成笔记真香
如何提升系统性能,设计出一个靠谱的系统是每一个架构师或者正在往架构师方向进阶的同僚们都需要考虑的问题。公司所处的行业,业务场景决定了你设计的系统演进过程,不过万变不离其宗,系统设计和优化的思想都是相通的(当然如果你刚入行没多久,目前肯定还不需要苦恼这种问题,但是工作用不到,不代表面试不问)。
|
7月前
|
消息中间件 Java 程序员
阿里巴巴高并发架构到底多牛逼?是如何抗住淘宝双11亿级并发量?
众所周知,在Java的知识体系中,并发编程是非常重要的一环,也是面试的必问题,一个好的Java程序员是必须对并发编程这块有所了解的。
|
消息中间件 Java 程序员
阿里巴巴高并发架构到底多牛逼?是如何抗住淘宝双11亿级并发量?
众所周知,在Java的知识体系中,并发编程是非常重要的一环,也是面试的必问题,一个好的Java程序员是必须对并发编程这块有所了解的。 然而不论是哪个国家,什么背景的 Java 开发者,都对自己写的并发程序相当自信,但也会在出问题时表现得很诧异甚至一筹莫展。
|
SQL NoSQL 关系型数据库
【并发】高并发下库存超卖问题如何解决?
【并发】高并发下库存超卖问题如何解决?
2483 0
|
设计模式 架构师 算法
这个时代,达不到百万以上并发量都不叫高并发!!收藏学以致用
成为一名年薪百万的顶尖架构师,实现财富自由,是大多数JAVA高级程序员的职业追求。 这不仅是技术发展的趋势,同时也是个人职业价值的体现。 但最终能否成为IT架构中的「灵魂人物」,做出亿级用户量的产品、搭建承载百万级并发的架构,还要取决于你能不能翻过并发量这道坎。
|
网络协议 Java C++
TCP网络编程模型从入门到实战中等篇,单服务器多个用户的简单并发版本, 从多进程 到多线程 到 线程池 版本服务器实现...直到最终解决面试经典C10k高并发服务器设计
TCP网络编程模型从入门到实战中等篇,单服务器多个用户的简单并发版本, 从多进程 到多线程 到 线程池 版本服务器实现...直到最终解决面试经典C10k高并发服务器设计
TCP网络编程模型从入门到实战中等篇,单服务器多个用户的简单并发版本, 从多进程 到多线程 到 线程池 版本服务器实现...直到最终解决面试经典C10k高并发服务器设计
下一篇
DataWorks