并发编程三大特性(6)

x33g5p2x  于2021-08-23 转载在 Java  
字(3.6k)|赞(0)|评价(0)|浏览(345)

一 说明

本文直接让你读懂多线程情况下的三大特性和happens-before关系,get核心技能,如果基础不够可以查阅作者并发编程栏目文章。

二 原子性

原子性 其本质意思是不可分割, 是指对一系列操作时,这些操作只有全部执行(success),或者全部不执行(fail),不存在第三种情况;当初学习数据库相关知识的时候大家应该都知道若数据库支持事物操作,那么其有四大特性,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)持久性(Durability);同理多线程的的原子性其思路跟数据库的操作的原子性是一致的。举个例子(银行转账),如果你向A先生转账100元,在这个过程中,第一步先要从你的账户中划出100元,第二步将100元划到A先生的账户;在这个例子中所谓的原子操作是指要么转账成功100元划至A先生账户(commit),要么转账失败钱退回你的账户(rollback);在这个例子中的非原子性操作是指你钱转出去了,中途程序出问题,A先生的账号没有收到100元;多线程中的原子性定义就是:对一系列操作,这些操作一旦开始进行就必须直到结束,中间不能切换至其他线程,要么操作成功,要么操作失败

2.1 非原子性举例剖析

示例代码:

public class Atomicity {

    private int i;

    public int getCount(){
        return i++;
    }
}

反汇编结果:

public class com.youku1327.base.thread.Atomicity {
  public com.youku1327.base.thread.Atomicity();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int getCount();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field i:I
       5: dup_x1
       6: iconst_1
       7: iadd
       8: putfield      #2                  // Field i:I
      11: ireturn
}

我们重点看第2步至第8步;第2步是变量 i 加载进栈;第5步是 复制了一份 变量 i 重新入栈;第6步是加载常量1进栈;第7步对变量 i 和 常量 1进行相加 操作;第8步是将加1后的变量 i 重新赋值给原来的变量 i 并且出栈;很明显 i++ 这个操作在这个过程中进行了3个步骤(加载变量,变量和常量相加,变量出栈重新赋值); 在java的JMM模型中,我们可以这样认为,先将变量 i 提取到本地内存,然后进行 副本变量 i 和 常量 1 相加操作,最后将本地内存刷至主存;那么在多线程的情况下,就有可能 2 个线程都拿到了变量 i 加载至 本地内存,都对 变量副本 i 和常量 1进行相加,最后同时刷至主存,那么本来应该是i的值是3,最终的结果 i的值是2;可以预见同时有2个线程可以对变量i进行操作,那么我们就认为这个操作是非原子性;

2.2 非原子性常见例子

  1. i ++ ;2.1已经具体分析;
  2. i = i + 1 ;同 i++ 原理相近,可以同时有2个线程对变量副本 i 进行加 1 操作;
  3. i = x ; 非原子性操作是 x 赋值给 i ,这个过程x 变量是不确定的,如果x等于 x++,那么延用上面的分析其也是非原子性操作;

2.4 JMM8大特性说明

  1. lock:作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
  2. unlock: 作用于主内存中的变量,它把一个处于锁定状态的变量释放出来;
  3. read:作用于主内存的变量,它把一个变量的值从主内存传输到线程的本地内存中,以便后面的load动作使用;
  4. load:作用于本地内存中的变量,它把read操作从主内存中得到的变量值放入本地内存中的变量副本
  5. use:作用于本地内存中的变量,它把本地内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
  6. assign:作用于本地内存中的变量,它把一个从执行引擎接收到的值赋给本地内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  7. store:作用于本地内存的变量,它把本地内存中一个变量的值传送给主内存中以便随后的write操作使用;
  8. write:作用于主内存的变量,它把store操作从本地内存中得到的变量的值放入主内存的变量中。

2.5 原子性总结

官方文档描述如下:
Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
Reads and writes are atomic for all variables declared volatile (including long and double variables).

  1. i=1 是原子性操作;
  2. read , load , use , assign store , write ,单个操作都是原子性;组合操作非原子性;
  3. 要是变量具备原子性可以使用 syncronized , lock锁机制 和 java.util.concurrent.atomic.*封装的原子类型就是变量具有原子性;
  4. 对于引用变量和大多数原始变量来说,读和写是原子性(除了long类型和double类型);
  5. 对于使用了volatile声明的变量读和写是原子性(包括long,double类型);
  6. volatile 不具备使变量具有原子性;

三 可见性

3.1 可见性的定义

可见性指在多线程情况下,如果一个线程对变量进行了修改,其他线程是可见的,能读取到变量最新的改变结果就是可见性;以JMM模型来分析就是多个线程同时将主存的变量x提取到本地内存,当有线程A对这个变量x进行修改,其他线程是可见的,那么其他线程只能等待线程A将本地内存刷至主存,然后重新获取变量x; 如果其他线程对变量x的修改不可见,那么就有可能多个线程都对变量x做了修改,然后都刷至主存,引发多线程缓存不一致问题;

3.2 可见性总结

  1. 使用syncronized能保证变量的可见性;
  2. 显示lock 能保证变量的可见性;
  3. 使用 volatile 能保证变量的可见性;

四 有序性

4.1 有序性的定义

重排序是指JMM模型中允许编译器对代码进行重排序进行优化;在单线程情况下不会发生问题,在多线程情况下会影响程序执行的正确性;多线程情况下如果能保证重排序不会影响程序的正确性就是程序的有序性

4.2 重排序

 private void getOrder(){
        int x = 0;
        int y = 1;
        int z = x + y;
    }

我们以最简单的示例来理解重排序,经过编译器优化后进行重排序那么有可能 x 的赋值,落后 于 y 的赋值,如果程序是有序性的 z 因为 依赖于 x , y , 所有 z 的赋值永远都在 x , y 赋值之后执行;如果程序不是有序性那么z 的赋值有可能发生在 x, y 的赋值之前,这就会导致程序出现异常;as-if-serial 语义中 就是单线程中无论 程序怎么重排序,都不会影响程序的最终结果,编译器永远不会对存在依赖关系的变量进行重排序,也跟这个是同样的道理

4.3 有序性总结

  1. 使用 volatile 能保证程序有序性;
  2. 使用syncronized能保证程序的有序性;
  3. 使用lock能保证程序的有序性;

4.3 happens-before 原则

happens-before 是指两个操作之间的执行顺序,可以是一个线程,也可以是多个线程进行操作;JMM模型中 happens-before 关系是可以保证程序的可见性;其具体意思指:A操作 happens-before B, 那么A 操作发生于 B操作之前,A 操作的结果是对B可见,JMM允许编译重排序,只要程序的结果正确,那么编译器可以不按照 happens-before 关系严格执行

  1. 程序顺序原则:即编译器可以使用重排序对代码进行优化编译,编写在后面的代码又能优于编写在前面的代码先执行,但不影响程序的正确性;
  2. 监视器规则:即必须等一个线程释放锁,另一个线程才有机会获得锁的拥有权;
  3. volatile 变量规则: 即线程A对变量进行修改,这个过程是可见的,线程B必须重新去主存中获取变量;意指对一个变量的写操作必须发生在读操作之前;
  4. 线程启动原则:即线程的start()方法必须优先于这个线程其他行为;
  5. 传递性原则:即 A happens-before B,B happens-before C ,那么 A happens-before C;
  6. 线程的join()规则:即线程A join 线程 B , 那么 B happens-before A;
  7. 线程中断规则:即一个线程若捕获到中断信号,其必先执行 interrupt()方法;
  8. 线程终结规则:即线程的所有行为都优于线程的死亡;
  9. 对象终结规则:即对象的初始化优于finalized()方法;

相关文章