2016 - 2024

感恩一路有你

concurrenthashmap如何保证效率 java编程,如何彻底理解volatile关键字?

浏览量:2108 时间:2023-06-17 12:20:16 作者:采采

java编程,如何彻底理解volatile关键字?

谢谢邀请~!下面解释了用法、注意事项和基本原理!

JMM基础-计算机原理

Java内存模型被称为Java内存模型,简称JMM。JMM在计算机(RAM)中定义了Java虚拟机(JVM)的工作模式。JVM是整个计算机的虚拟模型,所以JMM属于JVM。

在计算机系统中,寄存器是L0缓存,其次是L1、L2和L3(其次是内存、本地磁盘和远程存储)。缓存越高,存储空间越小,速度越快,成本越高。越往下,存储空间越大,速度越慢,成本越低。

从上到下,每一层都可以看作是下一层的缓存,即L0寄存器是L1一级缓存的缓存,

L1是L2的宝藏,等等;每一层的数据都来自下一层。

在目前的CPU上,一般来说,L0、L1、L2、L3都是继承自CPU,L1也分为一级数据缓存和一级指令缓存,分别用于存储数据和对数据进行指令解码。每个核都有独立的算术处理单元、控制器、寄存器、L1缓存和L2缓存,然后一个CPU的多个核共享最后一层CPU缓存L3。

CPU的缓存一致性解决方案

分为以下两种方案

总线锁(每次总线被锁定,都是悲观锁)

缓存锁(仅锁定缓存的数据)

《M:I(无效):,将stor:解锁主存变量,解锁后其他线程可以锁定该变量。

Java内存模型带来的问题

1、能见度问题

运行在左CPU的线程将对象obj从主存复制到其CPU缓存中,并将对象obj的count变量改为2,但这个变化对于运行在右CPU的线程是不可见的,因为这个变化并没有被刷新到主存中。

在多线程环境中,如果一个线程第一次读取一个共享变量,它首先从主内存中获取它。变量,然后存储在工作存储器中,以后只需要读取工作存储器中的变量。同样,如果变量被修改,新的值会先写入工作内存,然后刷新到内存中,但最新的值什么时候刷新到主存中是不确定的,一般来说是很快的,但具体时间未知。要解决共享对象的可见性问题,我们可以使用volatile关键字或lock。

2.竞争问题

线程A和线程B共享一个对象obj。假设线程A将变量从主存读入自己的缓存,线程B也将变量读入自己的CPU缓存,两个线程都加了1。此时,加1操作执行了两次,但两次都在不同的CPU缓存中。

如果二加一的运算是串行执行的,那么变量会在原来的值上加2,最后主存里的值就是3。那么图中的二加一运算是并行的。无论是线程A还是线程B先把计算结果刷新到主存,主存都只会增加一次,变成2。虽然有二加一运算,但是我们可以用同步的代码块来解决上面的问题。

3.再订购

除了共享内存和工作内存带来的问题,还有重新排序的问题。当执行程序时,编译器和处理器经常重新排序指令以提高性能。

重新排序分为三种类型:

(1)编译器优化的重新排序。

(2)指令级并行重排序

(3)记忆系统的重新排序

①数据依赖性

数据依赖:如果两个操作访问同一个变量,并且两个操作中有一个是写操作,那么这两个操作之间就存在数据依赖。

依赖关系分为以下三种类型:

从上图可以明显看出,A和C有数据依赖,B和C也有数据依赖,但是A和B之间没有数据依赖,如果重新安排A和C或者B和C的执行顺序,就会改变程序的执行结果。

显然,无论如何重新排序,都要保证代码在单线程下正确运行,甚至是单线程下,更不用说讨论多线程的并发性了,所以我们提出了as-if -serial的概念。

4、仿佛连载

无论如何重新排序(编译器和处理器提高并行度),一个(单线程)程序的执行结果都是无法改变的。编译器、运行时和处理器都必须遵循模拟串行语义。

A和C之间有数据依赖,B和C之间也有数据依赖,所以在最后的执行指令序列中,C可以 t在A和B之前重新排序(如果C在A和B之前,程序的结果会改变)。但是A和B之间没有数据依赖,编译器和处理器可以重新安排A和B之间的执行顺序..

仿佛系列语义菜单线程化的程序是有保护的,遵循as-if-serial语义的编译器、运行时和处理器会让我们觉得单线程程序好像是按照程序的顺序执行的。As-if-srial语义使得单线程程序不必担心重新排序会干扰它们或内存可见性。

5.记忆障碍

Java编译器会在适当的位置插入一个内存屏障来生成指令序列,禁止某些类型的处理重新排序,这样程序就可以按照我们预期的进程执行。

①保证具体操作的执行顺序。

②影响一些数据(或者一条指令的执行结果)的内存可见性。

编译器和CPU可以对指令进行重新排序,确保最终得到相同的结果,并试图优化性能。插入一个内存屏障会告诉编译器和CPU,没有指令可以用这个内存屏障指令重新排序。

内存屏障做的另一件事是强制刷新各种CPU缓存。例如,写屏障会在其自身屏障之前清除写入缓存的数据,因此CPU上的任何线程都可以读取这些数据的最新版本。

JMM将内存屏障指令分为四类:

储存量障碍是一个全方位的障碍,同时具有其他三个障碍的效果。

挥发性关键字介绍

1.确保可见性

当读取一个volatile变量时,您总是可以看到这个volatile变量的最后一次写入(由任何线程执行)。

让 让我们先看看下面的代码:

InitFlag没有用volatile关键字修饰;

以上结果是:

解释一个线程改变initFlag的状态,而另一个线程可以 I don'我看不见。

如果加上volatile关键字呢?

结果如下:

让 让我们看看通过汇编实现代码的最终底层实现:

volatile编写的内存语义如下:

当写入一个易变变量时,JMM会将线程对应的本地内存中的共享变量值刷新到主内存中。

当读取一个易变变量时,JMM会使线程对应的本地内存失效。线程接下来将从主内存中读取共享变量。

例如:

如果我们用volatile关键字修饰flag变量,那么实际上:线程A写完flag变量后,线程A在本地内存中更新的两个共享变量的值都被刷新到主存中。

读取标志变量后,本地存储器B中包含的值已被设置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读操作会导致本地内存B和主存享变量的值变得一致。

如果我们分两步写volatile和读volatile,总之,在由读取器线程B读取一个可变变量之后,在由写入器线程A写入该可变变量之前,所有可见的共享变量的值将立即变得对读取器线程B可见..

2.原子数

Volatile不保证变量的原子性;

运行结果如下:

因为计数

有三种操作:

(1)读取变量计数

(2)将count变量的值加1。

(3)再次将计算值赋给变量count。

来自JMM的记忆分析:

让 让我们分析一下为什么易挥发的I can 不能保证字节码的原子性。

javap :字节码视图

其实我这个操作主要可以分为三个步骤:(组装)

将volatile变量的值读取到local,增加变量的值,并将local的值写回给其他线程查看。

从加载到存储到内存屏障有四个步骤。最后一步,jvm让这个最新变量的值对所有线程可见,也就是最后一步让所有CPU核都得到最新的值,但是中间的步骤(从加载到存储)是不安全的,如果中间其他CPU修改了值,就会丢失。

3.整齐

(1)易变重排序规则表

①当第二个操作是易变写时,无论第一个操作是什么,都不能重新排序。这个规则确保了在volatile写入之前的操作不会在volatile写入之后被编译器重新排序。

②当第一个操作是volatile reading时,无论第二个操作是什么,都不能重新排序。这个规则确保在volatile读取之后的操作不会在volatile读取之前被编译器重新排序。

③当第一个操作是易失性写,第二个操作是易失性读时,不能重新排序。

(2)易失性记忆障碍

(1)易变的写作

Storestore barrier:对于这样一个语句,store1 storestore store2,保证store1的写操作在store2和后续写操作执行之前对其他处理器可见。(也就是说,如果出现storestore障碍,store1指令肯定会在store2之前执行,CPU不会对store1和store2重新排序。)

Storeload barrier:对于这样的语句store1 storeload load2,在执行load2和所有后续读取操作之前,store1的写入保证对所有处理器可见。(也就是说,如果出现storeload障碍,store1指令肯定会在load2之前执行,CPU不会重新安排store1和load2。命令

②易变读数

在每个易失性读取操作后插入一个LoadLoad屏障。在每个易失性读取操作后插入一个loadstore屏障。

Loadload barrier:对于这样的语句,load1 Load load2,保证load1要读取的数据在load2要读取的数据和后续的读取操作被访问之前被读取。(也就是说,如果出现loadload障碍,load1指令肯定会在load2之前执行,CPU不会对load1和load2重新排序。)

Loadstore barrier:对于这样一个语句,load1 loadstore store2,在store2和后续的写操作被刷出之前,load1要读取的数据保证被完全读取。(也就是说,如果存在loadstore障碍,那么load1指令肯定会在store2之前执行,CPU不会对load1和store2重新排序。)

挥发的实现原理

挥发的实现原理

?通过对OpenJDK中unsafe.cpp源代码的分析,会发现会有前缀 "Locke CHO 6-@ . com "在由volatile关键字修改的变量中。

?锁前缀,锁不是记忆屏障,但可以完成类似记忆屏障的功能。Lock锁定CPU总线和缓存,可以理解为CPU指令级的锁。

?同时,指令会直接将当前处理器缓存行的数据写入系统内存,这个写回操作会使缓存在这个地址的数据在其他CPU中失效。

?具体来说,它首先锁定总线和缓存,然后执行下面的指令,最后在释放锁定后将缓存中的所有脏数据刷新回主存。当锁锁定总线时,其他CPU的读写请求将被阻塞,直到锁被释放。

【欢迎密切关注@京京京京京,希望对你有帮助】

java concurrenthashmap put的时候要加锁吗?

不需要锁。Java ConcurrentHashMap在内部实现了锁机制,ConcurrentHashMap类包含两个静态内部类,HashEntry和Segment。HashEntry用于封装映射表的键/值对;Segment用来充当锁,每个Segment对象守护整个hash映射表的几个桶。每个桶都是由几个HashEntry对象链接的链表。ConcurrentHashMap实例包含几个Segment对象的数组。

内存 CPU 变量 数据

版权声明:本文内容由互联网用户自发贡献,本站不承担相关法律责任.如有侵权/违法内容,本站将立刻删除。