JAVA基础——volatile与JMM

每天进步一点点,大家好,我是大龄码农。

在讲解线程同步与通信时,提到了关键字volatile的可见性可以让线程之间通信。那么我们今天就来说说它的原理,但在这之前,先说一个非常重要的知识:JMM

JMM全称是Java内存模型,描述的是一组规则或规范,通过这组规范定义程序中各个变量的访问方式。它最初是为了解决多处理器架构下的缓存一致性问题。

我们知道,线程是程序运行的最小单位,它们共享进程的资源。每个线程在创建时,都有自己的工作内存,用于存储线程的私有数据,而JMM规定所有变量都存储在主内存,主内存是共享的内存区域,所有线程都可以访问,但是线程对变量的操作必须在工作内存中进行。

说明:图片来源于网络。

对于上面的操作,首先线程将变量obj.num从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,线程不能直接操作主内存中的变量。工作内存中存储着主内存中的变量副本拷贝,但是线程之间无法访问对方的工作内存,所以线程通信必须通过主内存来完成。

JMM主要解决了3种问题

  • 可见性

线程通信存在的本地内存数据不一致问题,即缓存一致性问题

  • 原子性

处理器为优化程序,导致对代码乱序执行所带来的问题

  • 有序性

由于编译器对代码指令重排序所带来的问题

我们还以上面的图片为例,假设obj.num=10,线程1想要将obj.num修改为20,线程2想要读取obj.num,那么此时线程2读取的obj.num的值会是多少呢?答案是随缘,通过上面关于对变量的操作描述可知,线程2读取obj.num值完全取决于读取的时机。如果此时线程1已经将工作内存的值20写到了主内存中,那么线程2读取的obj.num值就是20,否则就是10。

 

针对这种情况,JMM定义了以下八种原子操作来完成内存的交互操作

1、lock(锁定)

lock作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

2、unlock(解锁)

unlock作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

3、read(读取)

read作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

4、load(载入)

load作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

5、use(使用)

use作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作。

6、assign(赋值)

assign作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

7、store(存储)

store作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

8、write(写入)

write作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

以CPU2的处理为例,线程的处理流程是读取(read)主内存的变量flag、将主内存的变量flag值载入(load)到线程的工作内存、当执行引擎使用到变量flag时执行use操作、当执行引擎给工作内存中的变量flag赋值时执行assign操作、锁定(lock)主内存的变量flag、使用store操作将工作内存中的变量flag值传输到主内存中、将工作内存中的变量flag的值写入(write)到主内存、解锁主内存的变量flag。

因为无法保证多条组合操作也是原子性的,所以,提供了锁定与解锁两个原子指令,同时JMM对这8种原子操作有如下同步规定:

1、不允许线程将一个没有assign的数据从工作内存同步到主内存

2、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量,也就是说,对一个变量进行use和store操作之前,必须先进行load和assign操作。

3、一个变量在同一时刻只允许一条线程对其进行lock操作,执行多次lock后,必须执行相同次数的unlock操作才会解锁。lock和unlock必须成对出现。

4、如果对一个变量执行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量之前,必须重新执行load或assign操作初始化变量的值。

5、如果一个变量没有被lock操作锁定,就不能对它执行unlock操作,也不能unlock一个被其他线程锁定的变量。

6、对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

7、read和load、store和write每组中的两个操作必须同时出现,即使用了read以后必须要load,使用了store以后必须要write

8、如果执行了assign操作,即将工作内存中的变量数据改变了之后,必须告知主内存

Happens-Before规则

在多线程环境下,为了保证程序的正确性和一致性,程序中的操作需要满足一定的顺序关系。规则包括以下几种:

  • 程序顺序规则(Program Order Rule)

对于单个线程中的操作,Happens-Before规则保证该线程中的每个操作按照程序顺序执行,即程序中的操作顺序一定是线程中的执行顺序。

  • 管程锁定规则(Monitor Lock Rule)

对于一个锁的解锁操作,Happens-Before规则保证该锁的下一个锁定操作一定能够看到该锁的解锁操作。

  • volatile变量规则(Volatile Variable Rule)

对于一个volatile变量的写操作,Happens-Before规则保证其他线程的读操作一定能够看到该变量的更新值。

  • 传递性规则(Transitivity)

如果操作A Happens-Before操作B,操作B Happens-Before操作C,那么操作A Happens-Before操作C。

  • 线程启动规则(Thread Start Rule)

对于一个线程的启动操作,Happens-Before规则保证该线程中的操作一定能够看到主线程中在该线程启动之前的操作。

  • 线程终止规则(Thread Termination Rule)

对于一个线程的终止操作,Happens-Before规则保证该线程中的操作一定能够看到该线程在终止之前的操作。

  • 线程中断规则(Thread Interruption Rule)

对于一个线程的中断操作,Happens-Before规则保证该线程中的操作一定能够看到该线程在中断之前的操作。

上面简单介绍了Java的内存模型,下面来说一下volatile是如何工作的:

被volatile修饰的变量,在写入时JMM将线程工作内存的值直接刷新到主内存中,在读取时JMM将线程工作内存的值设置为无效,直接从主内存中读取。

那么,volatile凭什么保证变量的可见性和程序的有序性呢?内存屏障。

内存屏障是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后,才可以开始执行此点之后的操作。

 

内存屏障是一种JVM指令,JMM的重排规则要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了JMM中的可见性和有序性,但volatile无法保证原子性。

内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果。 如此便可以阻止屏障两边的指令重排序

内存屏障是由4条CPU的屏障指令实现的

1、loadload()

读读屏障,先执行屏障前的读,后执行屏障后的读。保证后执行的线程在读取时,线程的工作内存内的相应数据失效,重新到主内存中获取最新的数据。在每个volatile读操作的后面插入一个LoadLoad屏障

2、storestore()

写写屏障,先执行屏障前的写,后执行屏障后的写。保证后执行的线程在写入之前,先执行的写操作已经刷新到主内存。在每个volatile写操作的前面插入一个StoreStore屏障

3、loadstore()

读写屏障,先执行屏障前的读,后执行屏障后的写。保证后执行的线程在写入之前,先执行的读取已经结束。在每个volatile读操作的后面插入一个LoadStore屏障

4、storeload()

写读屏障,先执行屏障前的写,后执行屏障后的读。保证后执行的线程在读取时,先执行的写操作已经刷新到主内存,同时读取线程的工作内存内的相应数据失效,重新到主内存中获取最新的数。在每个volatile写操作的后面插入一个StoreLoad屏障

最后说一下指令重排问题,重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序。如果不存在数据依赖关系,就可以重排序,如果存在数据依赖关系,就禁止重排序 。

注:重排序后的指令绝对不能改变原有程序的串行语义。

int a = 1;int b = 2;int c = a + b;

    说明:声明变量c时,由于需要使用变量a和b的值,所以必须在声明变量a和b的后面,而声明变量a和b的顺序是可以被改变的。

volatile有关禁止重排的行为:

1、volatile读之后的操作,都禁止重排序到volatile之前

2、volatile写之前的操作,都禁止重排序到volatile之后

3、volatile写之后volatile读,禁止重排序


 

实践

一、非原子性

我们改造一下synchronized那篇文章使用的例子AddTogetherTest,它有两个特点:

1、++运算符不是原子操作,是由取值、运算、赋值三个操作组成

2、关键字synchronized,保证了同一时间只有一个线程运行

说明:去掉方法上的synchronized修饰,使用volatile修饰变量,结果并没有得到20000,说明volatile并不保证原子性

二、可见性

设置对象的属性为volatile

读线程

写线程

测试类

说明:

1、这是一个根据同一对象的属性进行读写的例子,由于使用了volatile关键字,程序会进入到交替读写的无限循环中。

2、如果去掉volatile关键字,虽然一开始还会有读写的交替运行,但是很容易就出现等待的无限循环。

    总结:

1、介绍了Java内存模型的知识

2、介绍了volatile关键字的作用

3、用程序说明了volatile的知识

原创文章,作者:速盾高防cdn,如若转载,请注明出处:https://www.sudun.com/ask/82822.html

Like (0)
速盾高防cdn的头像速盾高防cdn
Previous 2024年5月31日 下午4:57
Next 2024年5月31日

相关推荐

  • 域名服务器的作用是什么?

    在我们日常使用互联网时,经常会听到域名服务器这个词,但是你知道它的作用是什么吗?或许你也曾好奇过什么是域名服务器,它又是如何工作的呢?今天,我们就来揭开这个神秘的面纱,一起探究域名…

    行业资讯 2024年3月25日
    0
  • 网站被攻击立案标准,网站被人攻击

    任何网站运营商最不想看到的就是他们的网站受到攻击。然而,随着互联网的发展,网站攻击已成为普遍现象。那么,如果你的网站受到攻击,你如何追踪攻击者的身份,这是一个很大的问题。显然,攻击…

    行业资讯 2024年5月16日
    0
  • 门户网站整站源码,门户网站源码来一品资源网

    由于门户是重要的信息展示平台,因此选择高度安全的门户源代码非常重要。同时,还应该考虑可维护性,比如是否有持续的更新和技术支持。 7. 了解源代码定价和许可方法 不同的门户网站可能有…

    行业资讯 2024年3月24日
    0
  • 什么是GPU?解析其概念与作用

    你是否听说过GPU?它是一种令人惊叹的技术,也是当今云服务器行业中备受瞩目的存在。那么,什么是GPU?它究竟有着怎样的概念与作用呢?让我们一起来揭开这个神秘的面纱,探究GPU在云服…

    行业资讯 2024年4月3日
    0

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注