偽共享簡(jiǎn)介

偽共享是什么

CPU Cache
眾所周知CPU處理速度與硬盤(pán)、內(nèi)存的訪問(wèn)速度相差過(guò)大,需要通過(guò)CPU緩存進(jìn)行磨合,否則會(huì)導(dǎo)致CPU整體吞吐量受到極大的影響。
而單一層緩存無(wú)論是價(jià)格、命中率、查找速度方面都是不能夠滿足要求的,因此現(xiàn)在很多CPU出現(xiàn)了三級(jí)緩存結(jié)構(gòu),訪問(wèn)速度如下:

CPU緩存延遲,單位是CPU時(shí)鐘周期,可以理解為CPU執(zhí)行一個(gè)指令的時(shí)間

其中L1是L2的子集,L2是L3的子集,L1到L3緩存容量依次增大,查找耗時(shí)依次增大,CPU查找順序依次是L1、L2、L3、主存。
L1與CPU core對(duì)應(yīng),是單核獨(dú)占的,不會(huì)出現(xiàn)其他核修改的問(wèn)題。一般L2也是單核獨(dú)占。而L3一般是多核共享,可能操作同一份數(shù)據(jù),那么就有可能出問(wèn)題。

Cache Line
現(xiàn)代CPU讀取數(shù)據(jù)通常以一塊連續(xù)的塊為單位,即緩存行(Cache Line)。所以通常情況下訪問(wèn)連續(xù)存儲(chǔ)的數(shù)據(jù)會(huì)比隨機(jī)訪問(wèn)要快,訪問(wèn)數(shù)組結(jié)構(gòu)通常比鏈結(jié)構(gòu)快,因?yàn)橥ǔ?shù)組在內(nèi)存中是連續(xù)分配的。

PS. JVM標(biāo)準(zhǔn)并未規(guī)定“數(shù)組必須分配在連續(xù)空間”,一些JVM實(shí)現(xiàn)中大數(shù)組不是分配在連續(xù)空間的。

緩存行的大小通常是64字節(jié),這意味著即使只操作1字節(jié)的數(shù)據(jù),CPU最少也會(huì)讀取這個(gè)數(shù)據(jù)所在的連續(xù)64字節(jié)數(shù)據(jù)。

緩存失效
根據(jù)主流CPU為保證緩存有效性的MESI協(xié)議的簡(jiǎn)單理解,如果一個(gè)核正在使用的數(shù)據(jù)所在的緩存行被其他核修改,那么這個(gè)緩存行會(huì)失效,需要重新讀取緩存。

False Sharing
如果多個(gè)核的線程在操作同一個(gè)緩存行中的不同變量數(shù)據(jù),那么就會(huì)出現(xiàn)頻繁的緩存失效,即使在代碼層面看這兩個(gè)線程操作的數(shù)據(jù)之間完全沒(méi)有關(guān)系。
這種不合理的資源競(jìng)爭(zhēng)情況學(xué)名偽共享(False Sharing),會(huì)嚴(yán)重影響機(jī)器的并發(fā)執(zhí)行效率。

偽共享示例

// 多個(gè)線程,每個(gè)線程操作一個(gè)VolatileLong數(shù)組中的元素
// VolatileLong是否進(jìn)行填充會(huì)影響最終結(jié)果
// 為填充時(shí)會(huì)產(chǎn)生偽共享問(wèn)題,運(yùn)行更慢,填充后不會(huì)
public class FalseShareTest implements Runnable {
    public static int NUM_THREADS = 4;
    public final static long ITERATIONS = 50L * 1000L * 1000L;
    private final int arrayIndex;
    private static VolatileLong[] longs;
    public static long SUM_TIME = 0l;
    public FalseShareTest(final int arrayIndex) {
        this.arrayIndex = arrayIndex;
    }
    public static void main(final String[] args) throws Exception {
        Thread.sleep(10000);
        // 多個(gè)線程操作多個(gè)VolatileLong
        for(int j=0; j<10; j++){
        // 初始化
            System.out.println(j);
            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();
            // 構(gòu)造并啟動(dòng)線程
            runTest();
            final long end = System.nanoTime();
            SUM_TIME += end - start;
        }
        System.out.println("平均耗時(shí):"+SUM_TIME/10);
    }
    private static void runTest() throws InterruptedException {
        // 創(chuàng)建每個(gè)線程, 每個(gè)線程操作一個(gè)VolatileLong
        Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new FalseShareTest(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;     // 注釋此行,結(jié)果區(qū)別很大
    }
}

VolatileLong是否使用6個(gè)long變量填充,結(jié)果相差很多。
使用填充,會(huì)避免偽共享,速度更快。

偽共享如何避免

Java8以下的版本
在Java8以下的版本中,可以使用填充的方式進(jìn)行避免,比如百度的snowflake實(shí)現(xiàn)中使用的PaddedAtomicLong:

/**
 * Represents a padded {@link AtomicLong} to prevent the FalseSharing problem<p>
 * 
 * The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:<br>
 * 64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)
 * 
 * @author yutianbao
 */
public class PaddedAtomicLong extends AtomicLong {
    private static final long serialVersionUID = -3415778863941386253L;

    /** Padded 6 long (48 bytes) */
    public volatile long p1, p2, p3, p4, p5, p6 = 7L;

    /**
     * Constructors from {@link AtomicLong}
     */
    public PaddedAtomicLong() {
        super();
    }

    public PaddedAtomicLong(long initialValue) {
        super(initialValue);
    }

    /**
     * To prevent GC optimizations for cleaning unused padded references
     */
    public long sumPaddingToPreventOptimization() {
        return p1 + p2 + p3 + p4 + p5 + p6;
    }

}

對(duì)象引用8字節(jié),使用了6個(gè)long變量48字節(jié)進(jìn)行填充,以及一個(gè)long型的值,一共64字節(jié)。
使用了sumPaddingToPreventOptimization方法規(guī)避編譯器或GC優(yōu)化沒(méi)使用的變量。

Java8及以上的版本
從Java8開(kāi)始原生支持避免偽共享,可以使用@Contended注解:

public class Point { 
    int x;
    @Contended
    int y; 
} 

詳見(jiàn)@Contended注解使用方法。

@Contended 注解會(huì)增加目標(biāo)實(shí)例大小,要謹(jǐn)慎使用。默認(rèn)情況下,除了 JDK 內(nèi)部的類(lèi),JVM 會(huì)忽略該注解。要應(yīng)用代碼支持的話,要設(shè)置 -XX:-RestrictContended=false,它默認(rèn)為 true(意味僅限 JDK 內(nèi)部的類(lèi)使用)。當(dāng)然,也有個(gè) –XX: EnableContented 的配置參數(shù),來(lái)控制開(kāi)啟和關(guān)閉該注解的功能,默認(rèn)是 true,如果改為 false,可以減少 Thread 和 ConcurrentHashMap 類(lèi)的大小。參加《Java性能權(quán)威指南》210 頁(yè)。

參考資料

偽共享(false sharing),并發(fā)編程無(wú)聲的性能殺手 - 博客園
偽共享(False Sharing)和緩存行(Cache Line) 大雜燴 - 簡(jiǎn)書(shū)

本文搬自我的博客,歡迎參觀!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • CPU緩存 CPU和主內(nèi)存之間有好幾層緩存,因?yàn)榧词怪苯釉L問(wèn)主內(nèi)存也是非常慢的。如果你正在多次對(duì)一塊數(shù)據(jù)做相同的運(yùn)...
    CXYMichael閱讀 429評(píng)論 0 0
  • 維基百科中對(duì)偽共享的定義如下: 其大致意思是:CPU的緩存是以緩存行(cache line)為單位進(jìn)行緩存的,當(dāng)多...
    zhong0316閱讀 7,113評(píng)論 0 6
  • 【轉(zhuǎn)自】https://yq.aliyun.com/articles/62865 什么是偽共享 CPU緩存系統(tǒng)中是...
    lxqfirst閱讀 265評(píng)論 0 1
  • 文章轉(zhuǎn)自《一篇對(duì)偽共享、緩存行填充和CPU緩存講的很透徹的文章》 認(rèn)識(shí)CPU Cache CPU Cache概述 ...
    SunnyMore閱讀 1,883評(píng)論 0 10
  • 周末又不放假的噩耗傳來(lái),郁悶透頂,還好下班跑回家美餐了一頓外婆的營(yíng)養(yǎng)晚餐,第二天高高興興上班啦!
    陳玲_0029閱讀 258評(píng)論 0 0

友情鏈接更多精彩內(nèi)容