并发编程并发不可预期结果的根本原因

提出问题

说到并发,我们首先应该给自己提出下面这三个问题:

  1. 产生并发的根本原因是什么?
  2. 会造成什么后果?
  3. 怎么去控制,处理并发达到我们预期的结果。
提出这三个问题之后,我们慢慢来看看一下几个知识点,这三问题自然迎刃而解。解答上面3个问题之前,我们需要对JVM的内存模型有所了解,在《JVM内存模型》一文中已经将JVM内存模型讲的很清楚了,对JVM内存模型不了解的同学可以先去看看,然后继续本文。

在线程的角度来说,内存分为共享内存和私有内存两个部分。线程访问一个共享区的资源时,会copy一份到私有内存操作栈中,进行计算处理,处理完成之后会在线程消亡前的某一个时机,将最终的结果刷新到共享内存中。在多线程的情况下,计算机允许多个线程同时运行。这样就会出现一个问题,缓存不一致性问题。

举例说明:A,B两个线程都需要访问数据data = 0,但是访问的时机是不可控的。现在A去共享内存区域读取了data,copy一份到私有内存,开始处理比如说+1操作,处理到一半的时候,B也去共享内存区域读取了data,也copy一份到了私有内存区域开始处理,因为A还没有做完+1操作,还没有将最新结果刷新到共享内存,所以B读取到的不是data=1而是0,这时候B也开始做+1操作,得到的结果也是data=1。现在A处理完了,将数据刷新到共享内存,data = 1,B也做完了,将数据刷新到共享内存data=1。最终结果data=1,如果说A,在B读取之前就已经处理完成,并刷新了共享内存中的数据,那B读取到的是data=1,那么结果就是2。最后得出结论:多线程访问共享内存中数据时,得到的结果是不可控制的。

通过上面的案例,我们解答了上面的1,2两个问题。1。产生并发的原因是,线程访问共享内存中的数据,需要copy一份处理完成后在不确定时机刷新共享内存,并且A,B之间更改数据的时候,彼此不知道,还有A,B谁先执行,什么时候执行都不可控。2。造成的结果就是最终结果不可控,不一定得到我们预期的结果。

下面给出一个具体案例,可以自己运行试试看结果:

线程:

/**
 * Created by PICO-USER on 2017/11/9.
 */

public class AdditionRun implements Runnable {
    public int count = 0;

    @Override
    public void run() {
        try {
            //休眠10毫秒,模拟耗时操作,以便等待其他线程也启动起来了
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count += 1;
    }
}

程序入口:

public class MyClass {

    public static void main(String[] args0) throws InterruptedException {

        AdditionRun additionRun = new AdditionRun();
        Thread thread = null;
        for (int i = 0; i < 1000; i++) {
            thread = new Thread(additionRun);
            thread.start();
        }
        //休眠2秒,以便1000个线程已经全部执行完成。
        Thread.sleep(2000);
        System.out.print("Count :" + additionRun.count);
    }
}

很简单的案例,启动了1000个线程都对count进行+1操作,最后1000个线程运行完之后,打印出结果。我运行了10次,每一次的结果都不一样。

并发三个重要概念

要解决并发的问题,需要先了解一下三个概念

  1. 原子性 一个或多个操作要么全部执行完并且执行过程中不会被打断,要么都不执行。
    举例说明:
    1.int i = 2; int a = i; int b = i+1;
    上面三个例子中是否都保证原子性呢?第一个保证原子性,直接将2赋值给常量i,整个过程不能再分,直接一步完成整个操作。第二个不保证原子性,因为它其实是分为了几个步骤,首先给a开辟内存,然后取出i的值,然后将i的值赋给a,这个过程是可以被中断的,不能保证整个过程能全部执行完毕,所以不能保证原子性。第三个不保证原子性,首先给b开辟内存,然后取出i的值,然后做+1操作得到返回值赋给b,这个过程同样可能被中断,不能保证整个过程能全部执行完。
  2. 可见性 可见性是说线程之间都需要访问同一个共享数据,当这个共享数据发生改变的时候,所有的线程都能自动知道,这儿不在给予举例说明了,上面的案例中两个线程处理count的时候,就是不可见的。
  3. 有序性 有序性是指代码的执行能根据我们书写代码的顺序进行执行。在编译器编译代码的时候,不一定会会有一个代码优化过程,我们称为“指令重排序”,编译器不保证代码的执行顺序是按照书写代码的顺序执行,但是会保证执行结果跟书写代码顺序结果一致。于多线程中来说,我们不能保证线程的执行顺序。
    举例:int a =1;a = 2; a=3;这三句代码,编译器在编译的时候,并不会每一条语句都进行编译,只会编译a = 3,因为前两条之间并没有跟a的值产生依赖关系,所以是无效代码,所以前两句代码编译器不会进行编译。
开篇提出的第三个问题,这儿就可以给出解答了,控制并发问题,其实就是要保证原子性,可见性,有序性。,当然了,别没有给出实例,和实际API方案进行解决,具体的解决方法,会在本系列后续文章中讲解!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章