伪共享 false sharing,顾名思义,“伪共享”就是“其实不是共享”。那什么是“共享”?多CPU同时访问同一块内存区域就是“共享”,就会产生冲突,需要控制协议来协调访问。会引起“共享”的最小内存区域大小就是一个cache line。因此,当两个以上CPU都要访问同一个cache line大小的内存区域时,就会引起冲突,这种情况就叫“共享”。但是,这种情况里面又包含了“其实不是共享”的“伪共享”情况。比如,两个处理器各要访问一个word,这两个word却存在于同一个cache line大小的区域里,这时,从应用逻辑层面说,这两个处理器并没有共享内存,因为他们访问的是不同的内容(不同的word)。但是因为cache line的存在和限制,这两个CPU要访问这两个不同的word时,却一定要访问同一个cache line块,产生了事实上的“共享”。显然,由于cache line大小限制带来的这种“伪共享”是我们不想要的,会浪费系统资源。

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

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

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

  Java Memory Layout Java内存布局,在项目开发中,大多使用HotSpot的JVM,hotspot中对象都有两个字(四字节)长的对象头。第一个字是由24位哈希码和8位标志位(如锁的状态或作为锁对象)组成的Mark Word。第二个字是对象所属类的引用。如果是数组对象还需要一个额外的字来存储数组的长度。每个对象的起始地址都对齐于8字节以提高性能。因此当封装对象的时候为了高效率,对象字段声明的顺序会被重排序成下列基于字节大小的顺序:

  • double (8字节) 和 long (8字节)
  • int (4字节) 和 float (4字节)
  • short (2字节) 和 char (2字节):char在java中是2个字节。java采用unicode,2个字节(16位)来表示一个字符。
  • boolean (1字节) 和 byte (1字节)
  • reference引用 (4/8 字节)
  • <子类字段重复上述顺序>

在了解这些之后,就可以在任意字段间用7个long来填充缓存行。伪共享在不同的JDK下提供了不同的解决方案。

  在JDK1.6环境下,解决伪共享的办法是使用缓存行填充,使一个对象占用的内存大小刚好为64bytes或它的整数倍,这样就保证了一个缓存行里不会有多个对象。

  1. package basic;
  2.  
  3. public class TestFlash implements Runnable {
  4.  
  5. public final static int NUM_THREADS = 4; // change
  6. public final static long ITERATIONS = 500L * 1000L * 1000L;
  7. private final int arrayIndex;
  8.  
  9. /**
  10. * 为了展示其性能影响,我们启动几个线程,每个都更新它自己独立的计数器。计数器是volatile long类型的,所以其它线程能看到它们的进展。
  11. */
  12. public final static class VolatileLong {
  13.  
  14. /* 用volatile[ˈvɑ:lətl]修饰的变量,线程在每次使用变量的时候,JVM虚拟机只保证从主内存加载到线程工作内存的值是最新的 */
  15. public volatile long value = 0L;
  16.  
  17. /* 缓冲行填充 */
  18. /* 37370571461 :不使用缓冲行执行纳秒数 */
  19. /* 16174480826 :使用缓冲行执行纳秒数,性能提高一半 */
  20. public long p1, p2, p3, p4, p5, p6, p7;
  21. }
  22.  
  23. private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
  24. static {
  25. for (int i = 0; i < longs.length; i++) {
  26. longs[i] = new VolatileLong();
  27. }
  28. }
  29.  
  30. public TestFlash(final int arrayIndex){
  31. this.arrayIndex = arrayIndex;
  32. }
  33.  
  34. /**
  35. * 我们不能确定这些VolatileLong会布局在内存的什么位置。它们是独立的对象。但是经验告诉我们同一时间分配的对象趋向集中于一块。
  36. */
  37. public static void main(final String[] args) throws Exception {
  38. final long start = System.nanoTime();
  39. runTest();
  40. System.out.println("duration = " + (System.nanoTime() - start));
  41. }
  42.  
  43. private static void runTest() throws InterruptedException {
  44. Thread[] threads = new Thread[NUM_THREADS];
  45.  
  46. for (int i = 0; i < threads.length; i++) {
  47. threads[i] = new Thread(new TestFlash(i));
  48. }
  49.  
  50. for (Thread t : threads) {
  51. t.start();
  52. }
  53.  
  54. for (Thread t : threads) {
  55. t.join();
  56. }
  57. }
  58.  
  59. /*
  60. * 为了展示其性能影响,我们启动几个线程,每个都更新它自己独立的计数器。计数器是volatile long类型的,所以其它线程能看到它们的进展
  61. */
  62. @Override
  63. public void run() {
  64. long i = ITERATIONS + 1;
  65. while (0 != --i) {
  66. longs[arrayIndex].value = i;
  67. }
  68. }
  69. }

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

  在jdk1.7环境下,由于java 7会优化掉无用的字段。因此,JAVA 7下做缓存行填充更麻烦了,需要使用继承的办法来避免填充被优化掉。把填充放在基类里面,可以避免优化(这好像没有什么道理好讲的,JAVA7的内存优化算法问题,能绕则绕)。

  1. package basic;
  2.  
  3. public class TestFlashONJDK7 implements Runnable {
  4.  
  5. public static int NUM_THREADS = 4;
  6. public final static long ITERATIONS = 500L * 1000L * 1000L;
  7. private final int arrayIndex;
  8. private static VolatileLong[] longs;
  9.  
  10. public TestFlashONJDK7(final int arrayIndex){
  11. this.arrayIndex = arrayIndex;
  12. }
  13.  
  14. public static void main(final String[] args) throws Exception {
  15. Thread.sleep(10000);
  16. System.out.println("starting....");
  17. if (args.length == 1) {
  18. NUM_THREADS = Integer.parseInt(args[0]);
  19. }
  20.  
  21. longs = new VolatileLong[NUM_THREADS];
  22. for (int i = 0; i < longs.length; i++) {
  23. longs[i] = new VolatileLong();
  24. }
  25. final long start = System.nanoTime();
  26. runTest();
  27. System.out.println("duration = " + (System.nanoTime() - start));
  28. }
  29.  
  30. private static void runTest() throws InterruptedException {
  31. Thread[] threads = new Thread[NUM_THREADS];
  32. for (int i = 0; i < threads.length; i++) {
  33. threads[i] = new Thread(new TestFlashONJDK7(i));
  34. }
  35. for (Thread t : threads) {
  36. t.start();
  37. }
  38. for (Thread t : threads) {
  39. t.join();
  40. }
  41. }
  42.  
  43. @Override
  44. public void run() {
  45. long i = ITERATIONS + 1;
  46. while (0 != --i) {
  47. longs[arrayIndex].value = i;
  48. }
  49. }
  50. }
  51.  
  52. class VolatileLong extends VolatileLongPadding {
  53.  
  54. public volatile long value = 0L;
  55. }
  56.  
  57. class VolatileLongPadding {
  58.  
  59. public volatile long p1, p2, p3, p4, p5, p6, p7;
  60. }

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

  1. package basic;
  2.  
  3. public class TestFlashONJDK8 implements Runnable {
  4.  
  5. public static int NUM_THREADS = 4;
  6. public final static long ITERATIONS = 500L * 1000L * 1000L;
  7. private final int arrayIndex;
  8. private static VolatileLong[] longs;
  9.  
  10. public TestFlashONJDK8(final int arrayIndex){
  11. this.arrayIndex = arrayIndex;
  12. }
  13.  
  14. public static void main(final String[] args) throws Exception {
  15. Thread.sleep(10000);
  16. System.out.println("starting....");
  17. if (args.length == 1) {
  18. NUM_THREADS = Integer.parseInt(args[0]);
  19. }
  20.  
  21. longs = new VolatileLong[NUM_THREADS];
  22. for (int i = 0; i < longs.length; i++) {
  23. longs[i] = new VolatileLong();
  24. }
  25. final long start = System.nanoTime();
  26. runTest();
  27. System.out.println("duration = " + (System.nanoTime() - start));
  28. }
  29.  
  30. private static void runTest() throws InterruptedException {
  31. Thread[] threads = new Thread[NUM_THREADS];
  32. for (int i = 0; i < threads.length; i++) {
  33. threads[i] = new Thread(new TestFlashONJDK8(i));
  34. }
  35. for (Thread t : threads) {
  36. t.start();
  37. }
  38. for (Thread t : threads) {
  39. t.join();
  40. }
  41. }
  42.  
  43. @Override
  44. public void run() {
  45. long i = ITERATIONS + 1;
  46. while (0 != --i) {
  47. longs[arrayIndex].value = i;
  48. }
  49. }
  50. }
  1.  
  1. @Contended
  1. class VolatileLong {
  2.  
  3.   public volatile long value = 0L;
    }

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

补充:

byte字节  bit位 1byte=8bit

volatile说明

  1. package basic;
  2.  
  3. public class TestVolatile {
  4.  
  5. public static int count = 0;
  6.  
  7. /* 即使使用volatile,依旧没有达到我们期望的效果 */
  8. // public volatile static int count = 0;
  9.  
  10. public static void increase() {
  11. try {
  12. // 延迟10毫秒,使得结果明显
  13. Thread.sleep(10);
  14. count++;
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19.  
  20. public static void main(String[] args) {
  21. for (int i = 0; i < 10000; i++) {
  22. new Thread(new Runnable() {
  23.  
  24. @Override
  25. public void run() {
  26. TestVolatile.increase();
  27. }
  28. }).start();
  29. }
  30. System.out.println("期望运行结果:10000");
  31. System.out.println("实际运行结果:" + TestVolatile.count);
  32. }
  33. }

volatile关键字的使用:用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值。但是由于操作不是原子性的,对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。

在java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。上面一幅图描述这些交互,过程如下:

  • read and load 从主存复制变量到当前工作内存
  • use and assign  执行代码,改变共享变量值(其中use and assign 可以多次出现) 
  • store and write 用工作内存数据刷新主存相关内容

但是这些操作并不是原子性,也就是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。对于volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的。例如假如线程1,线程2在进行read load操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6。线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6。导致两个线程即使使用volatile关键字修改之后,还是会存在并发的情况。

对于volatile修饰的变量,JVM虚拟机只能保证从主内存加载到线程工作内存的值是最新的。

参考博客:

[1] http://www.cnblogs.com/Binhua-Liu/p/5620339.html

多线程中的volatile和伪共享的更多相关文章

  1. java多线程中的volatile和synchronized

    package com.chzhao; public class Volatiletest extends Thread { private static int count = 0; public ...

  2. 彻底弄明白之java多线程中的volatile

    一. volatite 简述 Java 语言提供了一种稍弱的同步机制,即 volatile 变量.用来确保将变量的更新操作通知到其他线程,保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新. ...

  3. Java:多线程中的volatile

    一.为什么使用volatile 首先,通过一段简单的代码来理解为什么要使用volatile: public class RunThread extends Thread{ private boolea ...

  4. Java Volatile关键字 以及long,double在多线程中的应用

    概念: volatile关键字,官方解释:volatile可以保证可见性.顺序性.一致性. 可见性:volatile修饰的对象在加载时会告知JVM,对象在CPU的缓存上对多个线程是同时可见的. 顺序性 ...

  5. Cache Line 伪共享发现与优化

    https://yq.aliyun.com/articles/465504 Cache Line 伪共享发现与优化 作者:吴一昊,杨勇 1. 关于本文 本文基于 Joe Mario 的一篇博客 改编而 ...

  6. java中伪共享问题

    伪共享(False Sharing) 原文地址:http://ifeve.com/false-sharing/ 作者:Martin Thompson  译者:丁一 缓存系统中是以缓存行(cache l ...

  7. Java 中的伪共享详解及解决方案

    1. 什么是伪共享 CPU 缓存系统中是以缓存行(cache line)为单位存储的.目前主流的 CPU Cache 的 Cache Line 大小都是 64 Bytes.在多线程情况下,如果需要修改 ...

  8. 从缓存行出发理解volatile变量、伪共享False sharing、disruptor

    volatilekeyword 当变量被某个线程A改动值之后.其他线程比方B若读取此变量的话,立马能够看到原来线程A改动后的值 注:普通变量与volatile变量的差别是volatile的特殊规则保证 ...

  9. JUC源码学习笔记4——原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法

    JUC源码学习笔记4--原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法 volatile的原理和内存屏障参考<Java并发编程的艺术> 原子类源码基于JDK8 ...

随机推荐

  1. Oracle 10g安装教程

    首先下载安装文件,打开后文件结构如图所示: 安装之前请关闭Windows防火墙并断开网络. xp系统下直接双击运行(本经验以XP系统安装为例进行讲述). 如果是在win7上安装,如图:在setup文件 ...

  2. Configure a bridge interface over a VLAN tagged bonded interface

    SOLUTION VERIFIED February 5 2014 KB340153 Environment Red Hat Enterprise Linux 6 (All Versions) Red ...

  3. [DJANGO] excel十几万行数据快速导入数据库研究

    先贴原来的导入数据代码: 8 import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "www.setting ...

  4. 面向未来的友好设计:Future Friendly

    一年前翻译了本文的一部分,最近终于翻译完成.虽然此设计思想的提出已经好几年了,但是还是觉得应该在国内推广一下,让大家知道“内容策略”,“移动优先”,“响应式设计”,“原子设计”等设计思想和技术的根源. ...

  5. logstash服务启动脚本

    logstash服务启动脚本 最近在弄ELK,发现logstash没有sysv类型的服务启动脚本,于是按照网上一个老外提供的模板自己进行修改 #添加用户 useradd logstash -M -s ...

  6. Ubuntu(Linux) + mono + xsp4 + nginx +asp.net MVC3 部署

    折腾了一下,尝试用Linux,部署mvc3. 分别用过 centos 和 ubuntu ,用ubuntu是比较容易部署的. 操作步骤如下: 一.终端分别如下操作 sudo su ->输入密码 a ...

  7. HTPC家庭娱乐和XBOX未来发展畅想<另:创业工作机会>

    微软中国在上海举办新闻发布会,正式宣布Xbox One将于9月23日在中国开始销售,定价3699元起.这款早在2001年就发布的电视游戏机终于在经历了14年的等待后,进军中国大陆市场.此次Xbox O ...

  8. HTML5 & CSS3初学者指南(2) – 样式化第一个网页

    介绍 我们已经使用基本的 HTML 编写了一个网页.但是,写出来的 HTML 代码的网页看起来很平淡,没有吸引力. 如何改善这种很平淡的页面呢? 让我们开始使用网页的基本样式来改善页面效果,我们将会使 ...

  9. WEB 基础知识(一)

    1. 系统架构 1.1 B/S系统架构 1.2 C/S系统架构 1.3 对比与区别 1.3.1 概述 C/S结构,即Client/Server(客户机/服务器)结构,是大家熟知的软件系统体系结构,通过 ...

  10. pod Spec管理配置

    pod Spec 为自己的项目添加pod管理功能.前言: 上一篇文章中提到,因为自己在操作的时候遇到很多坑,所在在此做一个记录,同样也希望可以帮到在这个操作上遇到坑的人. 本文将采用配图和加文字的方式 ...