一男子给对象转账5000元,居然又退还了!

在并发编程中,所有问题的根源就是可见性、原子性和有序性问题,这篇文章我们就来聊聊原子性问题。

在介绍原子性问题之前,先来说下线程安全:

线程安全

我理解的线程安全就是不管单线程还是多线程并发的时候,始终能保证运行的正确性,那么这个类就是线程安全的。

其中在《Java并发编程实战》一书中对线程安全的定义如下:

当多个线程访问某个类时,不管运行是环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

为了保证线程安全,可能会有很多的挑战和问题,当我们了解了问题根源所在,问题也就迎刃而解了,接下来介绍线程安全三大特性之一的原子性。

原子性

原子,我想大家应该都有印象吧,在化学反应中不可再分的基本微粒就是原子,也就是不可分割。

同时事务的四大特性 ACID 中也有原子性,那么原子性究竟是什么呢?

原子性其实就是所有操作要么全部成功,要么全部失败,这些操作是不可拆分的,也可以简单地理解为不可分割性。

将整个操作视作一个整体是原子性的核心特征,这些操作就是原子性操作

接下来举个原子性操作在生活中的例子:

比如,wupx 今天刚发了 5100 元的工资,全身家当为 5100 元,huxy 目前余额还有 1000 元,此时 wupx 上交 5000 元,如果转账成功,则 huxy 的余额就变为了 6000 元,wupx 的余额为 100 元。

若转账失败,则转出去的余额会退回来,wupx 的余额仍然是 5100 元,huxy 的余额为 1000 元。

不会出现 wupx 的钱转出去了,huxy 的余额没有增加,或者 wupx 的工资没转出去,而 huxy 的余额却增加的情况。

wupx 上交工资给 huxy 的操作就是原子性操作,wupx 余额减少 5000 元,而 huxy 的余额增加 5000 元的操作是不可分割和拆分的,正如我们上面说到的:要么全部成功,要么全部失败。wupxhuxy 上交成功流程如下所示:

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。

到这里,我相信大家对原子性有了基本的了解,下面来聊下原子性问题。

原子性问题

原子性问题的核心就是线程切换导致的,因为并发编程中,线程数设置的数目一般会大于 CPU 核心数。

关于线程数的设置可以阅读:线程数,射多少更舒适?

每个 CPU 同一时刻只能被一个线程使用,而 CPU 资源分配采用的是时间片轮转策略,也就是给每个线程分配一个时间片,线程在这个时间片内占用 CPU 的资源来执行任务,当过了一个时间片后,操作系统会重新选择一个线程来执行任务,这个过程一般称为任务切换,也叫做线程切换或者线程上下文切换。

上图就是线程切换的例子,有 Thread-0Thread-1 两个线程,其中粉色矩形表示该线程占有 CPU 资源并执行任务,刚开始 Thread-1 执行一段时间,这段时间称为时间片,在该时间片内,Thread-1 会占有 CPU 资源并执行任务,当经过一个时间片后,Thread-1 会让出 CPU 资源,虚线部分表示让出 CPU,不占用 CPU 资源,CPU 会重新选择一个线程 Thread-0 来执行,CPU 会在 Thread-0Thread-1 之间来回切换,反复横跳。

下面通过一个例子来看下原子性问题,具体代码如下:

public class AtomicityDemo {

    private long count = 0;

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

calc() 方法中只有一个 count++ 操作,那么就是原子性的吗?

下面在 class 目录下使用 javap -c AtomicityDemo 就可以得到如下结果:

public class com.`wupx`.thread.AtomicityDemo {
  public com.`wupx`.thread.AtomicityDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: lconst_0
       6: putfield      #2                  // Field count:J
       9: return

  public void calc();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field count:J
       5: lconst_1
       6: ladd
       7: putfield      #2                  // Field count:J
      10: return
}

重点来看下 calc 方法,这些 CPU 指令大概可以分为如下三步:

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

关于 CPU 缓存可以阅读:原来 CPU 为程序性能优化做了这么多

操作系统的线程切换并不是一定是发生一条语句执行完成后,而可能是发生在任何一条 CPU 执行完成后。比如 Thread-0 执行完指令 1 后,操作系统发生了线程切换,两个线程都执行了 count++ 操作,但是最后的结果是 1 而不是 2,下面用图来表示这个过程。

通过上图,我们可以发现:Thread-1count=0 加载到 CPU 的寄存器后,发生了线程切换,此时内存中的 count 值为 0,Thread-0count=0 加载到 CPU 寄存器,执行 count++ 操作,并将 count=1 写到内存,此时,CPU 切换到 Thread-1,执行 Thread-1 中的 count++ 操作后,Thread-1 中的 count 值为 1,Thread-1count=1 写入内存,此时内存中的 count 值为 1。

因此,在并发编程中,若在 CPU 中存在正在执行的线程,正好 CPU 发生了线程切换,则可能会导致原子性问题,这就是导致并发编程问题的根源之一。

针对原子性问题,我们可以通过为操作加锁或者使用原子变量来解决,原子变量在 java.util.concurrent.atomic 包中,是 JDK 1.5 引入的,它提供了一系列的原子操作。

总结

这篇文章简要介绍了线程安全的概念,并详细介绍了线程安全的特性之一原子性,并针对原子性问题进行了分析。

只有掌握了引发原子性问题的根源,才能便于我们编写更加安全的并发程序。

欢迎大家留言讨论,分享你的想法。

参考

《Java并发编程实战》

Java并发编程实战

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 本篇文章的主要内容如下: 1、Java线程概念2、Android线程的实现3、线程的阻塞4、关于线程上下文切换5、...
    Sophia_dd35阅读 549评论 0 3
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,949评论 1 18
  • 1.解决信号量丢失和假唤醒 public class MyWaitNotify3{ MonitorObject m...
    Q罗阅读 871评论 0 1
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,438评论 1 15
  • 一.线程安全性 线程安全是建立在对于对象状态访问操作进行管理,特别是对共享的与可变的状态的访问 解释下上面的话: ...
    黄大大吃不胖阅读 828评论 0 3