当前位置: 首页 > 网络应用技术

Java伪 - 共享原理 - 深度分析和回避方法

时间:2023-03-05 21:44:03 网络应用技术

  本文介绍了伪共享的一代以及如何避免在Java中的伪共享,并伴有案例演示!

  缓存系统中的缓存存储在缓存线的单位中。当多线程修改彼此的自变量时,如果这些变量共享相同的缓存,则会导致相同的缓存彼此的性能失败。,有人将其描述为沉默的表演杀手。

  众所周知,CPU的操作速度远远快于主内存和硬盘的操作速度。为了解决计算机系统中主内存和CPU之间运行速度差的问题,避免直接访问内存或磁盘,并减少CPU的整体吞吐量的影响,因为速度差通常会在CPU和Main Memory -speed -speed Buffer结构(CACHE)之间添加级别或多级别。该缓存通常集成在CPU内部。,因此也称为CPU缓存。

  根据数据读取顺序和与CPU的接近度,CPU缓存可以分为第一个级别的缓存L1,第二级高速公路L2,并且一些高端CPU也具有第三级高速缓存L3。存储在每个缓存级别中的数据是下一个缓存的一部分。靠近CPU缓存,更快且较小。L1和L2只能由单独的CPU核能使用,L3由CPU核共享,并且所有CPU核心共享主内存。

  当CPU执行操作时,它首先转到L1以找到所需的数据,然后转到L2,然后转到L3。最后,如果没有缓存,您需要的数据将转到主内存。步行越远,操作时间越长。因此,如果您执行非常频繁的操作,则必须确保数据在L1缓存。

  在书中引用CPU缓存,内存和硬盘内存的层次结构,“对计算机系统的深度理解”:

  在现代多核体系结构中,物理CPU可以具有多个物理内核,并且每个核心都可以具有多个硬件线程:

  在计算机缓存系统中,它存储在缓存单位(缓存线)中。缓存线是CPU和主内存之间数据传输的最小单元。缓存线通常为64个字节。

  当将缓存线团队从内存到缓存复制时,缓存将为此缓存线创建一个条目。此缓存条目不仅包含复制的内存数据,即缓存线,还包含元数据,例如此行的位置内存中的数据。

  当CPU访问特定变量时,首先要查看CPU缓存中是否存在此变量。如果有的话,直接获取它,否则您将转到主内存以获取变量,然后在变量所在的内存区域中取一个缓存线大小。将模拟复制到缓存。缓存,它是一个内存块而不是单个变量,可以将多个连续变量存储在缓存线中。

  结果,连续存储的数据通常比随机访问更快。访问阵列的结构通常比链结构更快,因为阵列通常在内存中连续分配。注意:某些JVM实现介质不分配在连续空间中。

  如上图所示,可变x和y同时将可变x和y放在CPU的L1,L1,L3中。核心一次只能运行一个线程。

  当线程L使用core1更新变量x时,首先修改core1的第一个级别缓存变量x的缓存线。此时,在缓存一致性协议(MESI)下,core2变量x中的缓存线失败。然后,线程2在编写变量y时只能转到第二个缓存,它破坏了第一个级别的缓存。第一个 - 级别。缓存比辅助缓存快,这也表明多个线程无法修改它们同时使用的CPU中同一缓存中的变量。更糟糕的是,如果CPU只有一个级别的缓存,则会导致原因经常访问主内存。

  因为缓存和内存交换数据的单位是缓存线,它导致多个变量放在缓存线中。为了确保缓存数据的一致性,根据MESI协议,其他内核可以与相同的缓存数据过期。如果多个线程同时在缓存中编写不同的变量,尽管不同的线程在明亮的表面,因为数据经常在同一缓存中无效,因此它会无意中影响彼此的性能。这是伪-Sharing。因为很难看到它是否在代码中是假的,所以有人将其描述为无声的性能杀手。

  为了避免由于错误共享而从L1,L2,L3到主内存之间重复加载,我们可以使用数据填充其他字节以避免使用,即单个数据填充Cacheline。

  这种方法本质上是改变时间的一种方法。

  在JDK 8之前,通常可以通过手动字节填充代码来避免问题,也就是说,在创建变量时,使用填充字段填充变量所在的缓存线,以免在该变量中存储多个变量在同一cacheline中相同的缓存。douglea的JSR166的早期linkedtransferqueue.it用于解决伪共享和额外的字节填充。此外,该技术用于早期contrenthashmap,无锁复杂的框架破坏者。

  源代码的一部分在早期的链接transferqueue中如下:如下:

  与父类AtomicReference相比,此内部类PaddeDatomicReference仅是一件事,并且共享变量添加到64个字节中,一个对象的引用之一为4个字节。IT将15个变量添加到总计60个字节,Plus Plus,Plus Plus。父级值变量,总计64个字节(其中哪个对象存储器布局:对象内存布局,压缩指针,对象大小计算以及对象访问的Javadetailed解释中对象大小的计算)。

  道格·利(Doug Lea)使用64个字节方法来填充装满快速缓冲区的缓存线。避免将头节点和尾部节点加载到同一缓存线中,因此更换时的头部和尾部节点不会锁定。

  从JDK 8开始,评论提供了解决伪共享问题的问题。此外,此注释的类将自动弥补jdk8中使用的缓存。

  线程类中的三个变量默认为0,这三个变量将在threadlocalrandom类中使用。默认情况下,t竞争的注释仅用于Java Core类别,例如此类别。需要在用户级路径下使用,需要添加JVM参数:-XX:-LesterrictContended。填充宽度默认为128字节之前和之后。如果您需要自定义宽度,则可以设置-xx:doptendPaddingWidth参数。

  运行以下情况,您可以看到VolatiLelong2,VolatiLelong3比VolatiLelong多。实际上,VolatiLelong2 P0,P1 ...的填充变量对程序的影响很小。我们的目的是使不同的差异不同。挥发对象处于不同的缓存线,可以避免假冒共享,而更少或更少的字节可能会产生很小的影响。

  参考资料: