二、聊聊并发 — 深刻理解并发三问题

前言

上篇文章我们已经聊了线程安全,大概了解了对线程安全产生影响的重要因素是什么,我们还聊到了多线程的消息传递方式和内存交互方式,正因为这种交互方式使得共享变量在多线程之前存在可见性问题,除此之外还有处理器为了指令优化导致的重排序以及原子操作问题,那这一篇我们就来细聊一下并发的这三个问题,让大家对并发程序中的重排序、内存可见性以及原子性有一定的了解

指令重排序

重排序通常是编译器或内存系统或者是处理器为了优化程序性能而采取的对指令进行重新排序执行的一种手段。按照程序运行在不同阶段,大致可以将重排序分为三种:编译器优化重排序指令级并行重排序内存系统重排序

  1. 编译器优化重排序就是通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。

  2. 指令级并行重排序是现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统是不会进行重排序的,由于使用缓存和读/写缓冲区,这使得操作看上去可能是在乱序执行。

下面我们通过例子分析一下重排序对程序的影响。

public class ConcurrentTest{
    private static int x, y;
    private static int a , b ;
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; )   {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            CountDownLatch latch = new CountDownLatch(1);
            Thread one = new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                a = 1;
                x = b;

            });
            Thread other = new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                b = 1;
                y = a;
            });
            one.start();
            other.start();
            latch.countDown();
            one.join();
            other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }  
}
运行结果:
...
...
第445次 (1,0)
第446次 (0,1)
第447次 (1,0)
第448次 (0,1)
第449次 (0,0)

上述的代码我们模拟了两个线程并发的场景,如果按照正常的代码逻辑是不可能出现x==y==0,但在某一时刻出现了 x == y == 0的情况下,那我们就来分析一下。

<img src="https://user-gold-cdn.xitu.io/2020/3/21/170fa347b1be75c7?w=862&h=842&f=png&s=61202" alt="b.png" style="zoom:50%;" />

在循环开始的时候我们就设置了x、y、a、b的值,让它们一直为0,那我们就默认为他们的初始状态为0,按照我们上面所说,如果两个指令之间不存在数据依赖关系,编译器、处理器可能对指令进行优化,调整他们的执行顺序,假如这两个线程的执行的指令都被优化,进行了上图中的重排序,那么某一时刻线程A看到 a 的值为 0,所以y = 0;同理,线程B也是同样的道理,就有可能导致 y == x == 0。

我们前面也提到内存系统不会进行重排序,主要是因为线程A操作完变量以后,B线程不能及时看到A线程对变量修改后的结果,导致操作看上去是乱序的,那我们从内存可见性的角度再来分析一下。

<img src="https://user-gold-cdn.xitu.io/2020/3/21/170fa341a3da9dc0?w=890&h=852&f=png&s=51881" alt="bn.png" style="zoom:50%;" />

如上图所示,线程A、B操作共享变量,需要将共享变量拷贝一份副本到自己的工作内存中,操作结束以后,将修改后的值同步到主内存中,但这个过程中有很多没办法预料的结果发生(在不能控制线程的执行顺序情况下),假如线程A先执行,已经修改了变量b的值,但还没有同步到主内存中,此时线程B从主内存中读取的变量b的值还是0,最后x==y==0。假如A、B线程同时执行,都从主内存读取到变量的值,操作完成以后,将值同步到的主内存中,此时同步以后主内存中还是 x == y == 0。所以说,当没有任何其他措施的情况下,多线程去操作共享变量时,线程的执行顺序完全由处理器进行调度,产生的结果也是不可预知的。

内存可见性

在我看来内存可见性可以换一种说法,用数据一致性这个词来代替可能会比较好理解一些。作为软件开发人员,我们可能对数据一致性这个词比较敏感一点,数据一致性问题也是我们经常会遇到的。例如Redis作为缓存和数据库之间的数据一致性,计算机硬件架构中高速缓冲区域和系统主内存之间的数据一致性。感兴趣的朋友可以去看看高速缓冲区是什么 CPU高速缓冲区,其中提到了一个概念:缓存一致协议,也就是我们所说的数据一致性。想对缓存一致协议了解一下的同学可以看这里:缓存一致协议。等到我们了解了Java内存模型,我们会感觉到其实缓存一致协议和Java内存模型有很多相像之处。

看完上述所说的,你心里可能也许大概有一点明白了。Java作为高级语义,能够跨平台运行在不同的操作系统上,其实是通过虚拟机屏蔽了这些底层细节,定义了一套自己读写内存数据的规范,让开发人员不再需要关心硬件或操作系统中的缓存、内存交互问题。但是也抽象了主内存和本地内存的概念:所有的共享变量存在于主内存中,每个线程有自己的本地内存,线程读写共享数据也是通过本地内存交换的,所以可见性问题依然是存在的。我们这里说的本地内存并不是真的是一块给每个线程分配的内存,而是 JMM 的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。但是内存模型也给出了解决这些问问题的办法,我们后面再详细的聊一聊。

原子性

原子操作即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

在Java并发编程中,如果对一个共享变量的操作不是原子操作,那有可能得不到你想要的结果。当多个线程访问同一个共享变量,且对共享变量的操作不是原子操作,那可能存在一个线程执行这个操作执行一半,另一个线程也执行这个非原子的操作,这样就会导致两个线程执行结果有误。我们通过例子来看一下。

public class ConcurrencyTest {
   public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread, "A");
        Thread thread1 = new Thread(myThread, "B");
        Thread thread2 = new Thread(myThread, "C");
        Thread thread3 = new Thread(myThread, "D");
        Thread thread4 = new Thread(myThread, "E");
        thread.start();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

class MyThread implements Runnable {
    private volatile int count = 0;
    public void run() {
        //synchronized (this) {
        count++;
        System.out.println("线程" + Thread.currentThread().getName() + "计算,count=" + count);
        //}
    }
}
运行结果:
线程A计算,count=2
线程E计算,count=5
线程D计算,count=4
线程C计算,count=3
线程B计算,count=2

对此我们就不在这里对原子性展开探讨了,我会在后面的文章中详细的来说一下Java中的原子操作问题。

总结

这里主要聊了一下并发编程中重排序、原子性、可见性对其带来的影响,也让我们对此并发编程线程安全问题有了进一步认识。那下一篇文章我会来详细的聊一聊如何解决这些线程安全的问题,以及内存模型对此的解决方案。

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