并发编程关于原子性 可见性 有序性的一点小知识

  • Post author:
  • Post category:其他




流水账

今天(20201023)去朋友家看刚刚出生的小宝宝,真可爱,真乖。

期间抽空看了CSDN 2020 1024程序员节的岳麓书院直播会谈。

会议上大佬们硬货满满,很多内容都是我们程序员这个群体很关心,也get到了很多。学习不止,不要让自己活在自己的舒适圈。做一些自己认为有意义的事情,坚持下去。



给自己打气

每天写一点,好记性不如烂笔头,分享也是一种美。



并发编程相关理解



原子性



原子性理解

原子性理解里是这样的:

我们CPU对内存的变量进行的一些列操作就是原子操作。

原子操作大致会有如下一些

read(读取):从主内存读取数据

load(载入):将主线程数据写入工作内存

use(使用):从工作内存读取数据去使用计算

assign(赋值):将计算好的值赋值到工作内存

store(存储):将工作内存的数据写入主内存

write(写入):将store过去的便利值赋值给主内存的变量

lock(锁定):将主内存的变量加锁,标示为线程独占

unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定



感受一下原子性

有这样一段代码,我们大家来感受一下并发编程的原子性,预见到打印的结果应该是多少?

 	public static  int count = 0;
    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        for (int i = 0; i < 20; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) { 
                    count++; 
                }
            });
            threads[i].start();
        }
        try {
            for (Thread thread : threads) {
                thread.join();

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }

结果

本来应该我们想要的结果是200000


但是实际运行的结果是小于等于200000的!



出现小于等于200000的原因是为什么?

一般变量的一次线程的原子性操作流程是这样的

:root { --mermaid-font-family: "trebuchet ms", verdana, arial;}

read load

store write

主内存

工作内存use ->assign

理想情况下每个线程都是在另一个线程store write之后use ->assign。但实际上会出现这么一个情况…一个线程进行use的时候另外一个线程也是在use…这个时候就会出现无法保证原子性。



那么volatile可以保证原子性吗?

我们来了解一下volatile的原子性。

当对一个变量添加了volatile关键字我们就对这个变量启动了总线(MESI缓存一执性协议)嗅探

“就类似对变量启动一个观察者模式,一旦一个线程修改了变量,就会马上执行一次 store,write,其他线程cup就会通过总线嗅探让工作内存变量失效,然后执行一次read,load”

volatile会在store的时候加一个lock,成功write到主内存后unlock

从volatile的原理上看,依然会存在有多个线程read load 到同一个值并+1的情况。


因此volatile并不能保证原子性


感兴趣的可以用上面的代码添加volatile运行查看运行结果。



我们怎么保证变量的原子性?

采用synchronized进行加锁。

上面的代码18行修改如下


  synchronized (Atomicity.class) {
                        count++;
                    }



可见性

可见性理解里是这样的:

我们有一个多线程共享的变量。

如果一个线程在修改时其他线程能够马上刷新到这个修改后的值。



我们为什么需要用volatile,synchronized保证他的可见性?

:root { --mermaid-font-family: "trebuchet ms", verdana, arial;}

打印thread start

打印thread end

线程一开始

循环判断!isTrue如果为true则继续循环

线程一结束

等待2秒执行线程二

:root { --mermaid-font-family: "trebuchet ms", verdana, arial;}

打印thread1 start

打印thread1 end

线程二开始

修改isTrue=true

线程一结束

有下面一段代码,可以预见一下他打印的结果。

    public static boolean isTrue = false;
    public static  void main(String[] args) {
        new Thread(() -> {
            Log.d("thread start");
            while ( !isTrue) {
            }
            Log.d("thread end");
        }).start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            Log.d("thread1 start");
            isTrue = true;
            Log.d("thread1 end");
        }).start();

    }

按照我们所期望打印的结果应该是


   thread start
   ·····(两秒后)
   thread1 start
   thread1 end
   thread end
   

实际上我们程序打印的是

thread start
thread1 start
thread1 end

这是为什么呢?

结合我们上面所讲的原子性可知,由于我们的第一个线程先read load到工作区在操作的isTrue是false,在第二个线程尽管赋值了 并store write到主内存了,但是第一个线程的变量并没有失效,于是就没有read load操作,因此无法结果。就无法打印

thread end



有序性

有序性理解里是这样的:

我们的指令代码CPU在处理时一般是按照指令顺序执行,但有偶发CPU会因为一段重排序算法将原本的指令顺序改变达到一个更加高速的处理速度。

我们来感受一下,下面一段代码。


 static int x = 0, y = 0, a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000000; i++) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread one = new Thread(() -> {
                a = x;
                y = 1;
            });

            Thread other = new Thread(() -> {
                b = y;
                x = 1;
            });
            one.start();
            other.start();
            one.join();
            other.join();
            if (a == 0 && b == 1) { //机率最高
                System.out.println("one先执行了.");
            } else if (a == 1 && b == 0) {//机率第二
                System.out.println("other先执行了.");
            } else if (a == 0 && b == 0) {//机率很小
                System.out.println("两个竟然相同应该是并行了.");
            } else {//机率更小
                System.out.println("重排序了,one先执行并且重排序了先执行y=1,other先执行重排序了执行了x=1");
            }
        }
    }

我们依次将打印注释由代码顺序来看a=0,b=1执行是最多次数的。

其次是a=1,b=0

100万次可能有那么几次是a=0,b=0

在这里插入图片描述

1000万次都可能不会出现那么一次a=1,b=1,但是如果当cpu 就觉得重排序速度快的时候会出现,虽然我认为这可能是“抽风”…但在做并发编程的时候一定要考虑到。

禁止指令重排序有两个规则:

as-if-else:

简而言之就是说如果指令操作如果有关联(依赖)那么就不会指令重排序。

这个基本上如果你先的类似这种伪代码就不用指令重排序

		//单独的工作线程内
		tmp=a
		a = b
		b=a

言而总之就是:会影响到你的程序结果cpu是一定不会重排序的。

happens-before:

简而言之就是如果你操作的时候加了屏障(lock一下)那就不会指令重排序。

为什么synchronized和volatile能保证有序性!!

你就看作利用了happens-before…,加了锁…



代码链接

链接:

并发编程关于原子性 可见性 有序性的一点小知识

.



吐槽

这篇文章就写到这里了…

其实这篇文章昨天晚上回家9点多随便写一下应该半个一个多小时就搞定吧…想着写完洗洗睡,结果写着写着…凌晨2点多…还没写完就写完可见性…今天下午又啪啪啪的一顿敲键盘写完。

生命不休,学习不止吧。



版权声明:本文为muxaioxie原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。