起因
BaiduのFaがある読んで UID-発電の 過程でソース、私は非常に奇妙なコードを発見しました:
/ ** * {パディング表し@link FalseSharing問題<P>防止するAtomicLong}を * * CPUキャッシュライン一般に64バイトであるが、ここでパディング後のキャッシュラインのサンプルである:<BR> * 64バイト= 8バイト(オブジェクト参照)(ロングパディング)+ 6×8バイト+ 8バイト(長い値) * * @author yutianbao * / パブリック クラス PaddedAtomicLongが延びAtomicLongを{ プライベート 静的 最終 長いのserialVersionUID = -3415778863941386253L 。 / ** パッド入り6の長(48バイト)* / 公共 揮発 長いです P1、P2、P3、P4、P5、P6 = 7L ; / ** * {からコンストラクタ@link AtomicLong} * / パブリックPaddedAtomicLong(){ スーパー()。 } 公共 PaddedAtomicLong(長いはinitialValue){ スーパー(はinitialValue)。 } }
そして、6つのルックス揮発性long変数(赤マーク)の無効果があります。これは私がそれを自分で書いたコードであれば、私は自分自身がもっと振戦よりも書くことが考えられているだろう。
振戦は長い間固定されている場合でも、BaiduのFAとして、限りオープンソースです。確かに、まだ深い意味を持っています。それから私は、いくつかのクラスのノートを読んでこの文を参照してください。
FalseSharing問題を防ぐために
案の定、これらの変数は、問題のFalseSharingを解決するためには、効果がないわけではありません。
しかし、私は私がFalseSharingされるものを知っていないようだ、と思いましたか?問題を解決し、他は大きな問題に落ちました。
そこで彼は、ブログをたくさん読んで、インターネットに多くの情報を確認し、FalseSharingの予備的な理解を持っていること。ここで書くと同じ混乱を持つように、ヘルプの人々に希望します。
文化的背景
それは、1つまたは2つの単語が何かを行うことができないFalseSharingを、明確にして、あなたが何かを追加する必要性を理解するためにいくつかの背景知識を持っている必要があります。
コンピュータのストレージアーキテクチャ
図に示す待ち時間ハードウェアとCPUの異なるレベル間の相互作用という。より速く、CPUに近いです。
コンピュータが実行されている、CPUはコマンドを実行するための場所である、との命令は、いくつかのデータを読み書きが必要になります。実行プログラムデータをメインメモリに格納され、メインメモリはそれほどCPUとメインメモリとの間の速度差を解消するために、現代のコンピュータは、キャッシュ(L1L2L3)に組み込まれている、特に低速(相対的)でした。
次のように現代のコンピュータ設計のキャッシュ/メモリは、一般的に次のとおりです。
L1およびL2は、それぞれCPUコアの排他的であり、L3は、共有全体のCPUコア(単一のCPUアーキテクチャ)内のすべてありました。
外出先でメインメモリを見つけるために、その後L3->オーダーL2、L1に発見、に従ってデータにするとCPUへのアクセス、。
キャッシュライン
そしてCPUではないバイトで、データ交換プロセスをキャッシュします。しかし、キャッシュライン単位でのたびにアクセスします。
キャッシュラインは、実際には、固定サイズのメモリ空間、一般的に64バイトのセクションです。
ヶ月
このことは、あまりにも揮発性の研究の学生は、これは間の各キャッシュ・コヒーレンス・プロトコルを伝えているに慣れかもしれです。
コア自身の使用につきL1 L2ため、彼らは様々なキャッシュの整合性が問題であることをバインドされているので間で、共有変数の異なるコアの問題を含むことができます。MESIは、これらの問題を解決する方法です。
MESI一般原則下:
私は抜粋の説明にオンライン検索を見てここにいます:
MESIプロトコルでは、各状態は、4本のキャッシュラインを有している2が利用可能で示すビット、それらは:
M(変更):このデータの行は、データが変更され、有効であり、データメモリ内の不整合は、データのみに存在しますキャッシュを提示し、
E(排他):このデータの行は、有効な、一貫性のあるデータであり、メモリ、データがキャッシュ内にのみ存在し;
S(共有):このデータの行は、有効な、一貫性のあるデータであり、メモリのデータが存在しますキャッシュ内の多くの;
I(無効):この行のデータが無効です。
CORE0とコア1は、共有変数変数Aを使用しているならば、異なるキャッシュラインでそれらの分布にキャッシュの0,1 Aのコピーがあることが人気のポイント。
あなたはA、AはSである。CORE0とコア1の状態変数のキャッシュラインを変更しない場合
如果Core0修改了A的值,则此时Core0的Cache Line变为M,Core1 的Cache Line变为I。
这样CPU就可以通过CacheLine的状态,来决定是删除缓存,还是直接读取什么的。
伪共享
背景知识介绍完毕了,这样再说伪共享就不会显得太难以理解了。
先说一个场景:
你的代码里需要使用一个volatile的Bool变量,当做多线程行为的一个开关:
static volatile boolean flag = true; public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> { Integer count = 0; while (flag) { ++count; System.out.println(Thread.currentThread().getName() + ":" + count); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = false; }).start(); }
这段代码会声明一个flag为true,然后有10个工作线程会在flag为true时没100ms对count做个自增操作,然后输出。当flag为false时,就会结束线程。
还有一个线程A,会在1000ms后将flag置为false。
这里就是volatile的一个经典用法,可以保证多个线程对flag的可见性,不会因为线程A修改了flag的值,但是工作线程读取到的不是最新值而额外执行一些工作。
这段代码看起来是没有任何问题的,实际上跑起来也没有问题。
但是结合之前的背景知识,考虑一下flag所在的cache line,肯定还会有其他的变量(cache line 64字节,bool无法完整填充一个CacheLine)。
如果flag所在的CacheLine里还有一个频繁修改的共享变量,这时会发生什么?
很简单,就是flag所在的CacheLine被频繁置为不可用,需要清除缓存重新读取。flag在工作状态并没有被修改,但是仍然会被其他频繁修改的共享变量所影响。
这样就会带来一个问题,即使flag并没有被修改,但我们的工作线程很多时间都等于是在主存中读取flag的值,这样在高并发时会带来很大的效率问题。
以上就是所谓的 “FalseSharing” 问题。
解决办法
FalseSharing对于普通业务应用,基本没什么实际影响。但是对于很多超高并发的中间件(例如发号器),可能就会带来一定的性能瓶颈。所以这类项目都是需要关注这个问题的。
出现原因已经说清楚了,那么该如何解决呢?
其实答案就在文章的开头,那6个看上去没有任何含义的volatile long变量,就是用来解决这个问题的。
The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)
这行注释就说明了这6个变量是如何解决FalseSharing问题的:
CacheLine一般是64字节,64 = 8(对象本身的属性信息)+ 6*8(long占用8个字节) + 8 (AtomicLong本身带有一个long) 。
写了这6个看着无效的变量后,PaddedAtomicLong就会占用64个字节,正好填满一个CacheLine,这样就会被独自分配到一个CacheLine,这样就不存在FalseSharing问题了。
需要注意的是本来AtomicLong仅占用不到20字节,但是为了解决FalseSharing做了填充之后就占用64字节了,这样就会导致空间会膨胀很多。所以即使用的时候也要做好取舍。