Java多线程之volatile关键字,happens-before

2024-06-22 17:32

本文主要是介绍Java多线程之volatile关键字,happens-before,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1 volatile
    • 1.1 理解
    • 1.2 缓存
  • 2 happens-before原则
    • 2.1 JMM内存模型
    • 2.2 重排序
    • 2.3 什么是happens-before
    • 2.4 具体的规则

1 volatile

1.1 理解

Java语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。

之所以要单独提出volatile这个不常用的关键字原因是这个关键字在高性能的多线程程序中也有很重要的用途,只是这个关键字用不好会出很多问题。
首先考虑一个问题,为什么变量需要volatile来修饰呢?
要搞清楚这个问题,首先应该明白计算机内部都做什么了。比如做了一个i++操作,计算机内部做了三次处理:读取-修改-写入
同样,对于一个long型数据,做了个赋值操作,在32系统下需要经过两步才能完成,先修改低32位,然后修改高32位。

假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题,是这些步骤执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。
通过这个设想,就不难理解volatile关键字了。
volatile可以用在任何变量前面,但不能用于final变量前面,因为final型的变量是禁止修改的。也不存在线程安全的问题。

对于volatile, <The Java Language Specification Third Edition>是这样描述的

“A field may be declared volatile, in which case the Java memory model ensures that all threads see a consistent value for the variable.”
“… the volatile modifier guarantees that any thread that reads a field will see the most recently written value.” - Josh Bloch
意思是,如果一个变量声明为volatile, Java 内存模型保证所有的线程看到这个变量的值是一致的。
Josh Bloch 说 ”volatile描述符保证任意一个程序读取的是最新写的值“

点击此处了解更多关于volatile知识

1.2 缓存

有人会问,内存不是存放变量值的地方吗,线程T1写,然后线程T2读,怎么会出现不一致的情况呢。
缓存
实际上内存不是唯一存储变量的地方。CPU往往会把变量的值存放到缓存中。假如一个CPU,即使在多线程环境下也不会出现值不一致的情况。但是,在多CPU,或者多核CPU的情况就不是这样了。
如下图所示,在多个CPU情况下,每个CPU都有独立的缓存,CPU通过连接相互获取缓存内容。线程T1的可能运行在CPU 0上,它从内存中读取值放到缓存中做运算,比如执行方法foo;线程T2运行于CPU 1上,执行方法bar
在这里插入图片描述

void foo(void)
{a = 1;b = 1;}void bar(void)
{while (b == 0) continue;assert(a == 1);
}

在多CPU情况下,由于CPU各自缓存的原因,线程可能观察到不一致的变量值。
volitate标志通过CPU基本的指令,比如(mfence x86 Xeon 或 membar SPARC)添加内存界限,让缓存和内存之间的值进行同步。查了某些资料说是总线嗅探机制,当一个线程改变了内存中的值,其他线程监听到这个变量的值变了后,就会自动同步内存中最新的值
volatile的一个作用
由于volatile保证一些线程写的值,另外一些线程能够立即看得到。我们可以通过这一特性,实现信号或事件机制。比如下面程序里主线程可以发送信号(把stopSignal设为true), 把线程workerThread立即终止。

public class  WorkerOwnerThread{// field is accessed by multiple threads.private static  volatile boolean  stopSignal;private static  void  doWork(){while (!stopSignal){Thread t = Thread.currentThread(); System.out.println(t.getName()+ ": I will work until i get STOP signal from my Owner...");}System.out.println("I got Stop signal . I stop my work");}private static   void stopWork() {stopSignal = true;//Thread t = Thread.currentThread(); //System.out.println("Stop signal from " + t.getName()  );}public static void main(String[] args) throws InterruptedException {Thread workerThread = new Thread(new Runnable() {public void run() {doWork();    }});workerThread.setName("Worker");workerThread.start();//Main threadThread.sleep(100);stopWork();System.out.println("Stop from main...");}
}

2 happens-before原则

JDK5开始,java使用新的JSR -133内存模型。JSR-133提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间
happens-beforeJMM最核心的概念,所以在了解happens-before原则之前,首先需要了解java的内存模型。

2.1 JMM内存模型

java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量来完成隐式通信java中的共享变量是存储在内存中的,多个线程由其工作内存,其工作方式是将共享内存中的变量拿出来放在工作内存,操作完成后,再将最新的变量放回共享变量,这时其他的线程就可以获取到最新的共享变量。
在这里插入图片描述
从横向去看看,线程A和线程B就好像通过共享变量在进行隐式通信。这其中有很有意思的问题,如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了 脏读现象。
为避免脏读,可以通过同步机制(控制不同线程间操作发生的相对顺序)来解决或者通过volatile关键字使得每次volatile变量都能够强制刷新到主存,从而对每个线程都是可见的。
点击了解更多Java内存模型知识点

2.2 重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:如图,1属于编译器重排序,而2和3统称为处理器重排序
这些重排序会导致线程安全的问题,一个很经典的例子就是DCL问题。JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序。
在这里插入图片描述
(1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
(2)指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
(3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

2.3 什么是happens-before

JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。
具体的定义为:
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

2.4 具体的规则

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回
  • 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  • 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

注意:两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second

这篇关于Java多线程之volatile关键字,happens-before的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1084949

相关文章

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001

Java面试题:通过实例说明内连接、左外连接和右外连接的区别

在 SQL 中,连接(JOIN)用于在多个表之间组合行。最常用的连接类型是内连接(INNER JOIN)、左外连接(LEFT OUTER JOIN)和右外连接(RIGHT OUTER JOIN)。它们的主要区别在于它们如何处理表之间的匹配和不匹配行。下面是每种连接的详细说明和示例。 表示例 假设有两个表:Customers 和 Orders。 Customers CustomerIDCus

22.手绘Spring DI运行时序图

1.依赖注入发生的时间 当Spring loC容器完成了 Bean定义资源的定位、载入和解析注册以后,loC容器中已经管理类Bean 定义的相关数据,但是此时loC容器还没有对所管理的Bean进行依赖注入,依赖注入在以下两种情况 发生: 、用户第一次调用getBean()方法时,loC容器触发依赖注入。 、当用户在配置文件中将<bean>元素配置了 lazy-init二false属性,即让