logo头像
Snippet 博客主题

原子性、可见性以及有序性

本文于1016天之前发表,文中内容可能已经过时。

原子性、可见性以及有序性

  • 原子性:
    众所周知,原子是构成物质的基本单位,所以原子代表着不可分。
    即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    最简单的一个例子就是银行转账问题,赋值或者return。比如a = 1;return a;这样的操作都具有原子性
    原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作!
    加锁可以保证复合语句的原子性,sychronized可以保证多条语句在synchronized块中语意上是原子的。

  • 可见性:
    在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,
    多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。
    (这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数);同样在单核处理器中这样由于备份造成的问题同样存在!
    这样的优化带来的问题之一是变量可见性——如果线程t1与线程t2分别被安排在了不同的处理器上面,那么t1t2对于变量A的修改时相互不可见,如果t1A赋值,然后t2又赋新值,那么t2的操作就将t1的操作覆盖掉了,
    这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。

  • 非原子性操作
    类似a += b这样的操作不具有原子性,在某些JVMa += b可能要经过这样三个步骤:

    - 取出`a`和`b`
    - 计算`a+b` 
    - 将计算结果写入内存
    如果有两个线程`t1`,`t2`在进行这样的操作。`t1`在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是`t2`开始执行,`t2`执行完毕后`t1`又把没有完成的第三步做完。这个时候就出现了错误,
    相当于`t2`的计算结果被无视掉了。所以上面的买碘片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。                       
    

    类似的,像a++这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。

  • 有序性
    有序性:即程序执行的顺序按照代码的先后顺序执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    int i = 0;
    boolean flag = false;
    i = 1; //语句1
    flag = true; //语句2
    ```
    上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?
    不一定,为什么呢?这里可能会发生指令重排序`(Instruction Reorder)`。
      下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
      比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
      但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?
    再看下面一个例子:
    ```java
    int a = 10; //语句1
    int r = 2; //语句2
    a = a + 3; //语句3
    r = a*a; //语句4

    这段代码有4个语句,那么可能的一个执行顺序是:
    语句2->语句1->语句3->语句4
    那么可能不可能是这个执行顺序呢?语句2->语句1->语句4->语句3,这是不可能的,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,
    如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

    虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //线程1:
    context = loadContext(); //语句1
    inited = true; //语句2
    //线程2:
    while(!inited ){
    sleep()
    }
    doSomethingwithconfig(context);

    上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成,
    那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
    从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
      也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。


支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者

上一篇