伪共享和缓存行填充,Java并发编程还能这么优化!

简介: 关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题。

前言

关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题。否则不仅无法发挥多线程的优势,还可能比单线程性能还差。随着JAVA版本的更新,再各个版本上减少伪共享的做法都有区别,一不小心代码可能就失效了,要注意进行测试。这篇文章总结一下。

什么是伪共享
关于伪共享讲解最清楚的是这篇文章:http://developer.51cto.com/art/201306/398232.htm,我这里就直接摘抄其对伪共享的解释:

缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。

为了让可伸缩性与线程数呈线性关系,就必须确保不会有两个线程往同一个变量或缓存行中写。两个线程写同一个变量可以在代码中发现。为了确定互相独立的变量是否共享了同一个缓存行,就需要了解内存布局,或找个工具告诉我们。Intel VTune就是这样一个分析工具。本文中我将解释Java对象的内存布局以及我们该如何填充缓存行以避免伪共享。

image


图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

JAVA 6下的方案

解决伪共享的办法是使用缓存行填充,使一个对象占用的内存大小刚好为64bytes或它的整数倍,这样就保证了一个缓存行里不会有多个对象。这篇文章http://developer.51cto.com/art/201306/398232.htm提供了缓存行填充的例子:

public final class FalseSharing 
    implements Runnable 
{ 
    public final static int NUM_THREADS = 4; // change 
    public final static long ITERATIONS = 500L * 1000L * 1000L; 
    private final int arrayIndex; 

    private static VolatileLong[] longs = new VolatileLong[NUM_THREADS]; 
    static 
    { 
        for (int i = 0; i < longs.length; i++) 
        { 
            longs[i] = new VolatileLong(); 
        } 
    } 

    public FalseSharing(final int arrayIndex) 
    { 
        this.arrayIndex = arrayIndex; 
    } 

    public static void main(final String[] args) throws Exception 
    { 
        final long start = System.nanoTime(); 
        runTest(); 
        System.out.println("duration = " + (System.nanoTime() - start)); 
    } 

    private static void runTest() throws InterruptedException 
    { 
        Thread[] threads = new Thread[NUM_THREADS]; 

        for (int i = 0; i < threads.length; i++) 
        { 
            threads[i] = new Thread(new FalseSharing(i)); 
        } 

        for (Thread t : threads) 
        { 
            t.start(); 
        } 

        for (Thread t : threads) 
        { 
            t.join(); 
        } 
    } 

    public void run() 
    { 
        long i = ITERATIONS + 1; 
        while (0 != --i) 
        { 
            longs[arrayIndex].value = i; 
        } 
    } 

    public final static class VolatileLong 
    { 
        public volatile long value = 0L; 
        public long p1, p2, p3, p4, p5, p6; // comment out 
    } 
}

VolatileLong通过填充一些无用的字段p1,p2,p3,p4,p5,p6,再考虑到对象头也占用8bit, 刚好把对象占用的内存扩展到刚好占64bytes(或者64bytes的整数倍)。这样就避免了一个缓存行中加载多个对象。但这个方法现在只能适应JAVA6 及以前的版本了。

(注:如果我们的填充使对象size大于64bytes,比如多填充16bytes– public long p1, p2, p3, p4, p5, p6, p7, p8;。理论上同样应该避免伪共享问题,但事实是这样的话执行速度同样慢几倍,只比没有使用填充好一些而已。还没有理解其原因。所以测试下来,必须是64bytes的整数倍)

JAVA 7下的方案
上面这个例子在JAVA 7下已经不适用了。因为JAVA 7会优化掉无用的字段,可以参考:http://ifeve.com/false-shareing-java-7-cn/

因此,JAVA 7下做缓存行填充更麻烦了,需要使用继承的办法来避免填充被优化掉,这篇文章http://ifeve.com/false-shareing-java-7-cn/里的例子我觉得不是很好,于是我自己做了一些优化,使其更通用:

public final class FalseSharing implements Runnable {  
    public static int NUM_THREADS = 4; // change  
    public final static long ITERATIONS = 500L * 1000L * 1000L;  
    private final int arrayIndex;  
    private static VolatileLong[] longs;  

    public FalseSharing(final int arrayIndex) {  
        this.arrayIndex = arrayIndex;  
    }  

    public static void main(final String[] args) throws Exception {  
        Thread.sleep(10000);  
        System.out.println("starting....");  
        if (args.length == 1) {  
            NUM_THREADS = Integer.parseInt(args[0]);  
        }  

        longs = new VolatileLong[NUM_THREADS];  
        for (int i = 0; i < longs.length; i++) {  
            longs[i] = new VolatileLong();  
        }  
        final long start = System.nanoTime();  
        runTest();  
        System.out.println("duration = " + (System.nanoTime() - start));  
    }  

    private static void runTest() throws InterruptedException {  
        Thread[] threads = new Thread[NUM_THREADS];  
        for (int i = 0; i < threads.length; i++) {  
            threads[i] = new Thread(new FalseSharing(i));  
        }  
        for (Thread t : threads) {  
            t.start();  
        }  
        for (Thread t : threads) {  
            t.join();  
        }  
    }  

    public void run() {  
        long i = ITERATIONS + 1;  
        while (0 != --i) {  
            longs[arrayIndex].value = i;  
        }  
    }  
}

public class VolatileLongPadding {
    public volatile long p1, p2, p3, p4, p5, p6; // 注释  
}

public class VolatileLong extends VolatileLongPadding {
    public volatile long value = 0L;  
}

把padding放在基类里面,可以避免优化。(这好像没有什么道理好讲的,JAVA7的内存优化算法问题,能绕则绕)。不过,这种办法怎么看都有点烦,借用另外一个博主的话:做个java程序员真难。

JAVA 8下的方案
在JAVA 8中,缓存行填充终于被JAVA原生支持了。JAVA 8中添加了一个@Contended的注解,添加这个的注解,将会在自动进行缓存行填充。以上的例子可以改为:

public final class FalseSharing implements Runnable {  
    public static int NUM_THREADS = 4; // change  
    public final static long ITERATIONS = 500L * 1000L * 1000L;  
    private final int arrayIndex;  
    private static VolatileLong[] longs;  

    public FalseSharing(final int arrayIndex) {  
        this.arrayIndex = arrayIndex;  
    }  

    public static void main(final String[] args) throws Exception {  
        Thread.sleep(10000);  
        System.out.println("starting....");  
        if (args.length == 1) {  
            NUM_THREADS = Integer.parseInt(args[0]);  
        }  

        longs = new VolatileLong[NUM_THREADS];  
        for (int i = 0; i < longs.length; i++) {  
            longs[i] = new VolatileLong();  
        }  
        final long start = System.nanoTime();  
        runTest();  
        System.out.println("duration = " + (System.nanoTime() - start));  
    }  

    private static void runTest() throws InterruptedException {  
        Thread[] threads = new Thread[NUM_THREADS];  
        for (int i = 0; i < threads.length; i++) {  
            threads[i] = new Thread(new FalseSharing(i));  
        }  
        for (Thread t : threads) {  
            t.start();  
        }  
        for (Thread t : threads) {  
            t.join();  
        }  
    }  

    public void run() {  
        long i = ITERATIONS + 1;  
        while (0 != --i) {  
            longs[arrayIndex].value = i;  
        }  
    }  
}

@Contended
public class VolatileLong {
    public volatile long value = 0L;  
}

执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效。很多文章把这个漏掉了,那样的话实际上就没有起作用。

@Contended注释还可以添加在字段上,今后再写文章详细介绍它的用法。

(后记:以上代码基于32位JDK测试,64位JDK下,对象头大小不同,有空再测试一下)

参考

http://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html

http://mechanical-sympathy.blogspot.hk/2011/08/false-sharing-java-7.html

http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html

原文发布时间为:2018-07-12
本文作者:Binhua
本文来自云栖社区合作伙伴“Java架构沉思录”,了解相关信息可以关注“Java架构沉思录”。

相关文章
|
2月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
71 0
|
3月前
|
存储 算法 Java
Java内存管理深度剖析与优化策略####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,重点分析了堆内存的分配策略、垃圾回收算法以及如何通过调优提升应用性能。通过案例驱动的方式,揭示了常见内存泄漏的根源与解决策略,旨在为开发者提供实用的内存管理技巧,确保应用程序既高效又稳定地运行。 ####
|
2月前
|
存储 缓存 自然语言处理
SCOPE:面向大语言模型长序列生成的双阶段KV缓存优化框架
KV缓存是大语言模型(LLM)处理长文本的关键性能瓶颈,现有研究多聚焦于预填充阶段优化,忽视了解码阶段的重要性。本文提出SCOPE框架,通过分离预填充与解码阶段的KV缓存策略,实现高效管理。SCOPE保留预填充阶段的关键信息,并在解码阶段引入滑动窗口等策略,确保重要特征的有效选取。实验表明,SCOPE仅用35%原始内存即可达到接近完整缓存的性能水平,显著提升了长文本生成任务的效率和准确性。
172 3
SCOPE:面向大语言模型长序列生成的双阶段KV缓存优化框架
|
3月前
|
缓存 监控 前端开发
在资源加载优化中,如何利用浏览器缓存提升性能?
通过以上这些方法,可以有效地利用浏览器缓存来提升资源加载的性能,减少网络请求次数,提高用户体验和应用的响应速度。同时,需要根据具体的应用场景和资源特点进行灵活调整和优化,以达到最佳的效果。此外,随着技术的不断发展和变化,还需要持续关注和学习新的缓存优化方法和策略。
113 53
|
2月前
|
缓存 算法 搜索推荐
Java中的算法优化与复杂度分析
在Java开发中,理解和优化算法的时间复杂度和空间复杂度是提升程序性能的关键。通过合理选择数据结构、避免重复计算、应用分治法等策略,可以显著提高算法效率。在实际开发中,应该根据具体需求和场景,选择合适的优化方法,从而编写出高效、可靠的代码。
49 6
|
2月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
2月前
|
存储 Java
Java 11 的String是如何优化存储的?
本文介绍了Java中字符串存储优化的原理和实现。通过判断字符串是否全为拉丁字符,使用`byte`代替`char`存储,以节省空间。具体实现涉及`compress`和`toBytes`方法,前者用于尝试压缩字符串,后者则按常规方式存储。代码示例展示了如何根据配置决定使用哪种存储方式。
|
2月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
93 5
|
2月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
3月前
|
存储 缓存 安全
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
72 4

热门文章

最新文章