• 并发线程特性-可见性和有序性


    2 可见性

    2.1 什么是可见性

    可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,CPU就提供了
    L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。
    这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自
    己的工作内存,没有及时的同步到主内存,导致数据不一致问题。
    [image]
    可见性问题的代码逻辑

    1. private static boolean flag = true;
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t1 = new Thread(() -> {
    4. while (flag) {
    5. // ....
    6. }
    7. System.out.println("t1线程结束");
    8. });
    9. t1.start();
    10. Thread.sleep(10);
    11. flag = false;
    12. System.out.println("主线程将flag改为false");
    13. }

    2.2 解决可见性的方式

    2.2.1 volatile

    volatile是一个关键字,用来修饰成员变量。
    如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主内存操作
    volatile的内存语义:
    ● volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
    ● volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变

    其实加了volatile就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile修饰的属性,会在转为汇编之后
    后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:
    ● 将当前处理器缓存行的数据写回到主内存
    ● 这个写回的数据,在其他的CPU内核的缓存中,直接无效。
    总结:volatile就是让CPU每次操作这个数据时,必须立即同步到主内存,以及从主内存读取数据。

    1. private volatile static boolean flag = true;
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t1 = new Thread(() -> {
    4. while (flag) {
    5. // ....
    6. }
    7. System.out.println("t1线程结束");
    8. });
    9. t1.start();
    10. Thread.sleep(10);
    11. flag = false;
    12. System.out.println("主线程将flag改为false");
    13. }

    2.2.2 synchronized

    synchronized也是可以解决可见性问题的,synchronized的内存语义。
    如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主
    内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。

    1. private static boolean flag = true;
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t1 = new Thread(() -> {
    4. while (flag) {
    5. synchronized (MiTest.class){
    6. //...
    7. }
    8. System.out.println(111);
    9. }
    10. System.out.println("t1线程结束");
    11. });
    12. t1.start();
    13. Thread.sleep(10);
    14. flag = false;
    15. System.out.println("主线程将flag改为false");
    16. }

    2.2.3 Lock

    Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个
    同步到主内存的操作。
    Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。
    如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,
    同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉取。

    1. private static boolean flag = true;
    2. private static Lock lock = new ReentrantLock();
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t1 = new Thread(() -> {
    5. while (flag) {
    6. lock.lock();
    7. try{
    8. //...
    9. }finally {
    10. lock.unlock();
    11. }
    12. }
    13. System.out.println("t1线程结束");
    14. });
    15. t1.start();
    16. Thread.sleep(10);
    17. flag = false;
    18. System.out.println("主线程将flag改为false");
    19. }

    2.2.4 final

    final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读取final属性,值肯定是一样。
    final并不是说每次取数据从主内存读取,他没有这个必要,而且final和volatile是不允许同时修饰一个属性的
    final修饰的内容已经不允许再次被写了,而volatile是保证每次读写数据去主内存读取,并且volatile会影响一定的性能,就不需要同时修饰。

    3 有序性

    3.1 什么是有序性

    在Java中,.java文件中的内容会被编译,在执行前需要再次转为CPU可以识别的指令,CPU在执行这些指令时,为了提升执行
    效率,在不影响最终结果的前提下(满足一些要求),会对指令进行重排。
    指令乱序执行的原因,是为了尽可能的发挥CPU的性能。
    Java中的程序是乱序执行的。
    Java程序验证乱序执行效果:

    1. static int a,b,x,y;
    2. public static void main(String[] args) throws InterruptedException {
    3. for (int i = 0; i < Integer.MAX_VALUE; i++) {
    4. a = 0;
    5. b = 0;
    6. x = 0;
    7. y = 0;
    8. Thread t1 = new Thread(() -> {
    9. a = 1;
    10. x = b;
    11. });
    12. Thread t2 = new Thread(() -> {
    13. b = 1;
    14. y = a;
    15. });
    16. t1.start();
    17. t2.start();
    18. t1.join();
    19. t2.join();
    20. if(x == 0 && y == 0){
    21. System.out.println("第" + i + "次,x = "+ x + ",y = " + y);
    22. }
    23. }
    24. }

    单例模式由于指令重排序可能会出现问题:
    线程可能会拿到没有初始化的对象,导致在使用时,可能由于内部属性为默认值,导致出现一些不必要的问题

    1. private static volatile MiTest test;
    2. private MiTest(){}
    3. public static MiTest getInstance(){
    4. // B
    5. if(test == null){
    6. synchronized (MiTest.class){
    7. if(test == null){
    8. // A , 开辟空间,test指向地址,初始化
    9. test = new MiTest();
    10. }
    11. }
    12. }
    13. return test;
    14. }

    3.2 as-if-serial

    as-if-serial语义:
    不论指定如何重排序,需要保证单线程的程序执行结果是不变的。
    而且如果存在依赖的关系,那么也不可以做指令重排。

    1. // 这种情况肯定不能做指令重排序
    2. int i = 0;
    3. i++;
    4. // 这种情况肯定不能做指令重排序
    5. int j = 200;
    6. j * 100;
    7. j + 100;
    8. // 这里即便出现了指令重排,也不可以影响最终的结果,20100

    3.3 happens-before

    具体规则:   

    1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。 2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。   

    3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。   

    4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。   

    5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。  6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。   

    7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检
    测。   

    8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。 JMM只有在不出现上述8中情况时,才不会触发指令重排效果。
    不需要过分的关注happens-before原则,只需要可以写出线程安全的代码就可以了。

    3.4 volatile

    如果需要让程序对某一个属性的操作不出现指令重排,除了满足happens-before原则之外,还可以基于volatile修饰属性,从而对
    这个属性的操作,就不会出现指令重排的问题了。
    volatile如何实现的禁止指令重排?
    内存屏障概念。将内存屏障看成一条指令。
    会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。

  • 相关阅读:
    分布式架构搭建
    EKF之雅克比矩阵(一)
    魔兽世界地图插件制作代码
    UDP-Based 多路径乱序传输
    java计算机毕业设计雁门关风景区宣传网站源程序+mysql+系统+lw文档+远程调试
    郑州大学编译原理实验三算符优先分析算法JAVA
    I3C协议通讯详解
    Linux- 内存映射文件(Memory-Mapped File)
    mybatis-plus控制台打印sql(mybatis-Log)
    指纹浏览器有什么用?盘点指纹浏览器八大应用场景
  • 原文地址:https://blog.csdn.net/qq_45309297/article/details/134362282