原子性,可见性,有序性

2020-03-05  本文已影响0人  瀚海来客

转载自:
并发之可见性
并发之原子性
并发之有序性

image.png

01 可见性的阐述

可见性 的定义是:一个线程对共享变量的修改,另外一个线程能够立刻看到。

内存并不直接与Cpu打交道,而是通过高速缓存与Cpu打交道。

cpu  <——> 高速缓存  <———>  内存

通过一张图片来表示就是(多核):

image

02 原因分析

可见性问题都是由Cpu缓存不一致为并发编程带来,而其中的主要有下面三种情况:

2.1、线程交叉执行

image

2.2、重排序结合线程交叉执行

image

由图知:由于编译时改变了执行顺序,导致结果不一致;而两个线程的交叉执行又导致线程改变后的结果也不是预期值,简直雪上加霜!

2.3、共享变量更新后的值没有在工作内存及主存间及时更新

package com.itquan.service.share.resources.controller;
import java.time.LocalDateTime;
/** * @author :mmzsblog 
* @description:共享变量在线程间的可见性测试 
*/
public class VisibilityDemo {      
      // 状态标识flag    
      private static boolean flag = true;    
      public static void main(String[] args) throws InterruptedException {              
            System.out.println(LocalDateTime.now() + "主线程启动计数子线程");        
            new CountThread().start();        
            Thread.sleep(1000);        
            // 设置flag为false,使上面启动的子线程跳出while循环,结束运行            
            VisibilityDemo.flag = false;     
            System.out.println(LocalDateTime.now() + "主线程将状态标识flag被置为false了");   
   }    
  static class CountThread extends Thread {      
      @Override        
      public void run() {            
          System.out.println(LocalDateTime.now() + "计数子线程start计数");               
           int i = 0;          
          while (VisibilityDemo.flag) {              
                i++;            
          }            
        System.out.println(LocalDateTime.now() + "计数子线程end计数,运行结束:i的值是" + i);       
     }   
 }}

运行结果是:

image

从控制台的打印结果可以看出,因为主线程对flag的修改,对计数子线程没有立即可见,所以导致了计数子线程久久不能跳出while循环,结束子线程。

03 如何解决线程间不可见性

为了保证线程间可见性我们一般有3种选择:

3.1、volatile:只保证可见性

volatile关键字能保证可见性,但也只能保证可见性,在此处就能保证flag的修改能立即被计数子线程获取到。

此时纠正上面例子出现的问题,只需在定义全局变量的时候加上volatile关键字

    // 状态标识flag    private static volatile boolean flag = true;

3.2、Atomic相关类:保证可见性和原子性

将标识状态flag在定义的时候使用Atomic相关类来进行定义的话,就能很好的保证flag属性的可见性以及原子性。

此时纠正上面例子出现的问题,只需在定义全局变量的时候将变量定义成Atomic相关类

    // 状态标识flag    private static AtomicBoolean flag = new AtomicBoolean(true);

不过值得注意的一点是,此时原子类相关的方法设置新值和得到值的放的是有点变化,如下:

    // 设置flag的值    VisibilityDemo.flag.set(false);       
   // 获取flag的值    VisibilityDemo.flag.get()

3.3、Lock: 保证可见性和原子性

此处我们使用的是Java常见的synchronized关键字。

此时纠正上面例子出现的问题,只需在为计数操作i++添加synchronized关键字修饰

    synchronized (this) {        i++;    }

通过上面三种方式,得到类似如下的期望结果:

image

然而,接下来阿粉我要对其中的volatilesynchronized关键字做一番较为详细的解释。

04 可见性-volatile

Java内存模型对volatile关键字定义了一些特殊的访问规则,当一个变量被volatile修饰后,它将具备两种特性,或者说volatile具有下列两层语义:

针对第一点,volatile保证了不同线程对这个变量进行读取时的可见性,具体表现为:

附一张CPU缓存模型图:

image

综上所述:就是用volatile修饰的变量,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。

使用volatile无法保障线程安全,那么volatile的作用是什么呢?

其中之一:(对状态量进行标记,保证其它线程看到的状态量是最新值)

image

volatile关键字是Java虚拟机提供的最轻量级的同步机制,很多人由于对它理解不够(其实这里你想理解透的话可以看看happens-before原则),而往往更愿意使用synchronized来做同步。所以接下来阿粉我再说说synchronized关键字。

05 可见性synchronized

image

5.1、作用域

synchronized关键字的作用域有二种:

5.2、可用于方法中的某个区块中

除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

用法是:

synchronized(this){    /*区块*/}

它的作用域是当前对象;

5.3、不能继承

synchronized关键字是不能继承的,也就是说,基类的方法

synchronized f(){    // 具体操作}

在继承类中并不自动是

synchronized f(){    // 具体操作}

而是变成了

f(){    // 具体操作}

继承类需要你显式的指定它的某个方法为synchronized方法;

综上3点所述:synchronized关键字主要有以下这3种用法:

这三种用法就基本保证了共享变量在读取的时候,读取到的是最新的值。

5.4、JVM关于synchronized的两条规定:

从上面的这两条规则也可以看出,这种方式保证了内存中的共享变量一定是最新值。

但我们在使用synchronized保证可见性的时候也要注意以下几点:

上一篇 下一篇

猜你喜欢

热点阅读