并发编程——可见性与有序性

news2024/12/23 13:38:58

如果有兴趣了解更多相关内容,欢迎来我的个人网站看看:耶瞳空间

JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。JMM体现在以下几个方面:

  • 原子性:保证指令不会受到线程上下文切换的影响
  • 可见性:保证指令不会受cpu缓存的影响
  • 有序性:保证指令不会受cpu指令并行优化的影响

一:可见性

1.1:问题展示

可以先看下面一段代码:

public class Demo {
  static boolean run = true;

  public static void main(String[] args) {
    new Thread(() -> {
      while(run) {}
      System.out.println("停止循环");
    }).start();

    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("停止t");
    run = false;
  }
}

可以看到,即使主线程将run改为了false,子线程还是没有停止循环。

在这里插入图片描述

子线程刚开始的时候从主存读取了run的值到工作内存:

在这里插入图片描述

因为子线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率:

在这里插入图片描述

1秒之后,main线程修改了run的值,并同步至主存,而子线程是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。

在这里插入图片描述

1.2:问题解决

我们可以用volatile(易变关键字)解决问题,它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。不过这种方式也降低了程序的运行效率。

在这里插入图片描述

除此之外,还可以用synchronized解决该问题:

public class Demo {
  static boolean run = true;

  // 锁对象
  final static Object lock = new Object();

  public static void main(String[] args) {
    new Thread(() -> {
      while(true) {
        synchronized (lock) {
          if (!run) break;
        }
      }
      System.out.println("停止循环");
    }).start();

    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("停止t");
    synchronized (lock) {
      run = false;
    }
  }
}

在这里插入图片描述

synchronized实现可见性的原理:

  • 在释放锁之前一定会将数据写回主内存:一旦一个代码块或者方法被synchronized所修饰,那么它执行完毕之后,被锁住的对象所做的任何修改都要在释放之前,从线程内存写回到主内存。也就是说他不会存在线程内存和主内存内容不一致的情况。
  • 在获取锁之后一定从主内存中读取数据:同样的,线程在进入代码块得到锁之后,被锁定的对象的数据也是直接从主内存中读取出来的,由于上一个线程在释放的时候会把修改好的内容回写到主内存,所以线程从主内存中读取到数据一定是最新的。

synchronized与volatile的区别:

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

二:指令重排

JVM会在不影响正确性的前提下,根据情况调整语句的执行顺序。

先看下面的代码,对i和j赋值,先对i赋值再对j赋值其实跟先对j赋值再对i赋值没有区别,所以在真正执行的时候,这两个顺序都有可能。这种特性称之为指令重排。

static int i;
static int j;

// 在某个线程内执行如下赋值操作
i = ...;
j = ...;

指令重排存在的意义,其实说到底都是源于对性能的优化,CPU运行效率相比缓存、内存、硬盘IO之间效率有着指数级的差别,CPU作为系统的宝贵资源,那么如何更好的优化和利用这个资源就能提升整个计算机系统的性能。其实指令重排序就是一种来源于生活的优化思想,比如做菜,一般会把熟得最慢的菜放到最开始(比如煲汤),因为在等待这些菜熟的过程中(IO等待)我们(CPU)还可以做其它事情,这就是一种时间上的优化。在计算机领域也是一样,它也会根据指令的类别做一些优化,目的就是把CPU的资源利用起来,这样就能就能提升整个计计算机的效率。

三种重排序场景

  • 编译器重排序:针对程序代码语而言,编译器可以在不改变单线程程序语义的情况下,可以对代码语句顺序进行调整重新排序。
  • 指令集并行的重排序:这个是针对于CPU指令级别来说的,处理器采用了指令集并行技术来讲多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应的机器指令执行顺序。
  • 内存重排序:因为CPU缓存使用缓冲区的方式(Store Buffere)进行延迟写入,这个过程会造成多个CPU缓存可见性的问题,这种可见性的问题导致结果的对于指令的先后执行显示不一致,从表面结果上来看好像指令的顺序被改变了,内存重排序其实是造成可见性问题的主要原因所在。

指令重排序的原则(as-if-serial语义):编译器和处理指令也并非什么场景都会进行指令重排序的优化,而是会遵循一定的原则,只有在它们认为重排序后不会对程序结果产生影响的时候才会进行重排序的优化,如果重排序会改变程序的结果,那这样的性能优化显然是没有意义的。而遵守as-if-serial语义规则就是重排序的一个原则,as-if-serial 的意思是说,可以允许编译器和处理器进行重排序,但是有一个条件,就是不管怎么重排序都不能改变单线程执行程序的结果。

在复杂的多线程环境下,编译器和处理器是根本无法通过语义分析来知道代码指令的依赖关系的,所以这个问题只有写代码的人才知道,这个时候编写代码的人就需要通过一种方式显示的告诉编译器和处理器哪些地方是存在逻辑依赖的,这些地方不能进行重排序。所以在编译器层面和CPU层面都提供了一套内存屏障来禁止重排序的指令,编码人员需要识别存在数据依赖的地方加上一个内存屏障指令,那么此时计算机将不会对其进行指令优化。

不过因为不同的CPU架构和操作系统都有各自对应的内存屏障指令,为了简化开发人员的工作,避免开发人员需要去了解各种不同的底层的系统原理,所以在JAVA里面封装了一套规范,把这些复杂的指令操作与开发人员隔离开来,这套规范就是我们常说的Java内存模型(JMM),JMM定义了几个happens before原则来指导并发程序编写的正确性。程序员可以通过Volatile、synchronized、final几个关键字告诉编译器和处理器哪些地方是不允许进行重排序的。

三:volatile

内存屏障有两个作用:

  • 阻止屏障两侧的指令重排序
  • 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

内存屏障分为两种,读屏障(Load Barrier)和写屏障(Store Barrier):

  • 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据
  • 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见

java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能:

  • LoadLoad屏障:对于这样的语句Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1;LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1;StoreLoad;Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence),其内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:

  • 对volatile变量的写指令后会加入写屏障
  • 对volatile变量的读指令前会加入读屏障

写屏障(Store Barrier):保证在该屏障之前的,对共享变量的改动,都同步到主存当中

volatile static boolean ready = false;

public void actor2(I_Result r) {
	num = 2;
	ready = true;  // ready是volatile赋值带写屏障
	// 写屏障
}

读屏障(Load Barrier):保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

volatile static boolean run = false;

public void actor1(I_Result r) {
	// 读屏障
	// ready是volatile读取值带读屏障
	if (ready) {
		r.r1 = num + num;
	} else {
		r.r1 = 1;
	}
}

在这里插入图片描述

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。

volatile 性能:volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

四:happens-before

happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见:

  static int x;
  static Object m = new Object();
  
  public static void main(String[] args) {
    new Thread(() -> {
      synchronized(m) {
        x = 10;
      }
    }, "t1").start();

    new Thread(() -> {
      synchronized(m) {
        System.out.println(x);
      }
    }, "t2").start();
  }

线程对volatile变量的写,对接下来其它线程对该变量的读可见:

  volatile static int x;
  
  public static void main(String[] args) {
    new Thread(() -> {
      x = 10;
    }, "t1").start();

    new Thread(() -> {
      System.out.println(x);
    }, "t2").start();
  }

线程start前对变量的写,对该线程开始后对该变量的读可见:

  static int x;
  
  public static void main(String[] args) {
    x = 10;
    
    new Thread(() -> {
      System.out.println(x);
    }, "t2").start();
  }

线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束):

  static int x;
  
  public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
      x = 10;
    });
    t.start();
    
    t.join();
    System.out.println(x);
  }

线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或 t2.isInterrupted):

  static int x;

  public static void main(String[] args) {
    Thread t2 = new Thread(() -> {
      while (true) {
        if (Thread.currentThread().isInterrupted()) {
          System.out.println(x);
          break;
        }
      }
    }, "t2");
    t2.start();

    new Thread(() -> {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      x = 10;
      t2.interrupt();
    }, "t1").start();

    while (!t2.isInterrupted()) {
      Thread.yield();
    }
    System.out.println(x);
  }

对变量默认值(0,false,null)的写,对其它线程对该变量的读可见:

  volatile static int x;
  static int y;

  public static void main(String[] args) {
    new Thread(() -> {
      x = 20;
      y = 10;
    }, "t1").start();
    
    new Thread(() -> {
      // x=20对t2可见,同时y=10也对t2可见
      System.out.println(x);
      System.out.println(y);
    }, "t2").start();
  }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/390553.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web API

DOM API 1、选中页面元素 let elem document.querySelector(CSS选择器); console.log(elem); console.dir(elem); 2、事件 鼠标点击事件 onclick 鼠标移动事件 onmousemove 等等 事件源 .box,事件类型 onlick,事件处理方式 alert(hello) let d…

[Mybatis1]介绍与快速入门

文章目录 Mybatis概述 持久层 框架 Mybatis与JDBC对比 JDBC代码的缺陷 Mybatis简化JDBC Mybatis快速入门案例 整体案例项目结构 1.创建user表,添加数据 2.创建Maven项目,导入坐标 3.编写Mybatis核心配置文件 4.编写数据库返回对象的实体类 5. 编写S…

QML Button详解

1.Button简介 Button表示用户可以按下或单击的按钮控件。按钮通常用于执行一个动作,或回答一个问题。典型的按钮有确定、应用、取消、关闭、是、否和帮助。 Button继承自AbstractButton,提供了以下几种信号。 void canceled() //当按…

Python笔记 -- 列表

文章目录1、列表简介2、修改、添加、删除元素2.1、添加2.2、删除3、排序、倒序4、遍历列表5、创建数值列表6、列表切片7、列表复制8、元组1、列表简介 在Python中用方括号[]表示列表,用逗号隔开表示其元素 通过索引访问列表 names [aa,bb,cc,dd]print(names[0]) …

游戏项目中的程序化生成(PCG):算法之外的问题与问题

本篇讨论的是什么 从概念上讲,PCG(程序化生成)的含义很广:任何通过规则计算得到的内容,都可算作是PCG。但在很多游戏项目的资料,包括本篇,讨论PCG时特指是:用一些算法/工具(特别是H…

C语言-基础了解-13-C enum枚举

C enum枚举 一、C枚举 枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量。,它可以让数据更简洁,更易读。 枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。 定义一个枚举类型&a…

3.2 LED闪烁流水灯蜂鸣器

LED闪烁1.1 电路连接示意图LED采用低电平点亮的方式,利用ST-Link的3.3V进行供电。1.2程序设计1.21知识储备GPIO配置步骤步骤:1. 第⼀步,使⽤RCC开启GPIO的时钟2. 第⼆步,使⽤GPIO_Init()函数初始化GPIO3. 第三步,使⽤输…

JavaWeb--会话技术

会话技术1 会话跟踪技术的概述2 Cookie2.1 Cookie的基本使用2.2 Cookie的原理分析2.3 Cookie的使用细节2.3.1 Cookie的存活时间2.3.2 Cookie存储中文3 Session3.1 Session的基本使用3.2 Session的原理分析3.3 Session的使用细节3.3.1 Session钝化与活化3.3.2 Session销毁目标 理…

java坦克大战(1.0)

坦克大战 后面开始学习怎么使用java制造一个坦克大战游戏 但是不是直接开始做,而是随着这个游戏程序的制造,一边学习新知识融入到游戏中。包括多线程,反射,IO流… Java坐标体系 在几乎所有的坐标中都有一个x轴和y轴&#xff0c…

大数据项目实战之数据仓库:用户行为采集平台——第1章 数据仓库概念

第1章 数据仓库概念 数据仓库(Data Warehouse),是为企业制定决策,提供数据支持的。可以帮助企业改进业务流程、提高产品质量等。 数据仓库的输入数据通常包括:业务数据、用户行为数据和爬虫数据等 业务数据&#xf…

Java - 对象的比较

一、问题提出 前面讲了优先级队列,优先级队列在插入元素时有个要求:插入的元素不能是null或者元素之间必须要能够进行比较,为了简单起见,我们只是插入了Integer类型, 那优先级队列中能否插入自定义类型对象呢&#xf…

深入理解JDK动态代理原理,使用javassist动手写一个动态代理框架

文章目录一、动手实现一个动态代理框架1、初识javassist2、使用javassist实现一个动态代理框架二、JDK动态代理1、编码实现2、基本原理(1)getProxyClass0方法(2)总结写在后面一、动手实现一个动态代理框架 1、初识javassist Jav…

Dijkstra算法的入门与应用

目录 一、前言 二、Dijkstra算法 1、Dijkstra 算法简介 2、算法思想:多米诺骨牌 3、算法实现 4、例子 三、例题 1、蓝桥王国(lanqiaoOJ题号1122) 一、前言 本文主要讲了Dijkstra算法的概念、实现与一道模板例题。 二、Dijkstra算法…

RSTP基础要点(上)

RSTP基础RSTP引入背景STP所存在的问题RSTP对于STP的改进端口角色重新划分端口状态重新划分快速收敛机制:PA机制端口快速切换边缘端口的引入RSTP引入背景 STP协议虽然能够解决环路问题,但是由于网络拓扑收敛较慢,影响了用户通信质量&#xff…

分布式对象存储

参考《分布式对象存储----原理、架构以及Go语言实现》(作者:胡世杰) 对象存储简介 数据的管理方式 以对象的方式管理数据,一个对象包括:对象的数据、对象的元数据、对象的全局唯一标识符 访问数据的方式 可扩展的分…

useCallback、useMemo、React.memo

1、React.memo React.memo 是 React 中用于函数组件优化的高阶组件,可以在一定程度上减少组件的重渲染,提升应用性能。React.memo 的实现原理是对比组件的前后两次渲染传入的 props 是否相等,如果相等则不会触发重新渲染,否则会触…

使用 Nacos 搭建一个简单的微服务项目

Nacos Nacos 是阿里巴巴推出来的一个新开源项目,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 准备Nacos 将 nacos 安装成功之后,进入nacos的bin 目录下,通过命令sh startup.sh -m standalone启动nacos,然后…

ChatGPT概述:从模型训练到基本应用的介绍

ChatGPT概述:从模型训练到基本应用的介绍 目录 本文是对ChatGPT的由来、训练过程以及实际落地场景的解释,主要内容包括如下三个方面: 1、ChatGPT是什么 2、ChatGPT的原理 3、ChatGPT的思考 4、ChatGPT的应用 ChatGPT是什么 ChatGPT可能是近…

代码随想录算法训练营第四天| 24. 两两交换链表中的节点 、19.删除链表的倒数第N个节点、面试题 02.07. 链表相交 、142.环形链表II

24. 两两交换链表中的节点 24.两两交换链表中的节点介绍给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。思路上述是自己看到这道…

Zookeeper3.5.7版本——客户端命令行操作(命令行语法)

目录一、命令行语法二、help命令行语法示例一、命令行语法 命令行语法列表 命令基本语法功能描述help显示所有操作命令ls path使用 ls 命令来查看当前 znode 的子节点 [可监听]-w 监听子节点变化-s 附加次级信息create普通创建-s 含有序列-e 临时(重启或者超时消失…