卖服务器建网站,绍兴网站建设 微德福,南宁共建站,公司做一个网站内容如何设计方案存可以说是计算机领域最伟大的发明之一#xff0c;经常会有人问#xff0c;缓存是越多越好么#xff1f;一般人们都会斩钉截铁的回答不是。至于为什么#xff1f;往往无法直觉回答了#xff0c;可能会从缓存一致性#xff0c;空间占用等几个角度逐一分析。今天就来看看由…存可以说是计算机领域最伟大的发明之一经常会有人问缓存是越多越好么一般人们都会斩钉截铁的回答不是。至于为什么往往无法直觉回答了可能会从缓存一致性空间占用等几个角度逐一分析。今天就来看看由于一致性导致的缓存问题。在之前的文章中我们聊过JMM java的内存模型一定要有所了解不太清楚的同学可以看下前文链接https://www.cnblogs.com/jilodream/p/9452391.html可以知道线程并不是直接读写内存而是调用线程自己的工作空间。但这只是一个逻辑模型线程我们可以理解为cpu的核心工作空间所对应的位置一般是指cpu的缓存。就像下图这样jmmcpu目前主流的cpu就是每个核心有自己的多级缓存一般还会加一个共享缓存越靠近核缓存越小但越快成本也越大。java线程实际对应的就是这个核工作空间对应的就是这个缓存。如果你详细思考就会考虑到缓存中的数据时如何加载变量的。毕竟变量又长有短如何加载定位的一般来说我们将内存划分成若干的块(防盗连接本文首发自http://www.cnblogs.com/jilodream/ )每一块是64个字节主流是这个大小同时我们将缓存划分成若干的缓存行也是64个字节。cpu每次加载时不是按照某个变量加载而是将已经划分好的整块内容直接加载到缓存行中。因为从数据的使用经验来看一般我们在使用某个变量时很大可能会使用邻近变量这种缓存的预判加载提高了缓存的命中率。有小伙伴会有疑问会不会不同核的缓存行加载的数据跨了内存块了也就是A核的缓存行是 xyz变量B核的缓存行是yza变量。这是不会的缓存块是根据内存的地址和偏移量划分好的不会根据不同核来划分不同的边界的。cacheline1做过缓存设计的同学肯定知道在设计时一定要考虑数据一致性的问题。如果多份缓存以及主存之间的数据不一致就无法并发处理无法得到准确的结果。pscpu一般是通过MESI缓存一致性协议并且配合失效缓存队列等等来实现的感兴趣的读者可以查下相关内容从前文中的java内存模型中可以知道当volatile变量发生变化时java通过内存屏障来强制失效其它cpu核心中缓存。但是在真实情况下cpu是按照行来缓存变量的而不是单个变量此时标记失效的就是整个缓存行。那么就会出现类似一个情况线程1操作变量a线程2操作变量b根据缓存的加载机制1两者的均加载同一段缓存行。2当线程1 修改完变量a时通知其它线程失效该缓存行3线程2修改变量b发现缓存行失效重新加载缓存行修改完变量b后重新知会其它线程该行已经失效这样当线程1每次修改变量a线程2每次修改变量b时当前缓存都不断的需要重新加载本质上已经失去了缓存的意义还增加了缓存状态控制缓存重新加载的开销。这种在相同的缓存行的多个变量但是由于并发原因导致缓存不断失效无法利用缓存读取变量的场景我们就称之为伪共享。False Sharingcacheline2这种情况其实不仅仅是java其它语言甚至是多缓存的业务都会有类似的问题。即由于并发引起的缓存联动失效即使对我当前业务没有实际影响但是由于缓存一致性的协议设计我们判断当前缓存已经脏了。我们就需要重新加载。缓存的优势丧失成本却被无限放大。就像下边这个例子缓存类1 public class CacheA {2 volatile int a;34 volatile int b;5 }线程类复制代码1 public class Main {2 private static CacheA cache new CacheA();3 private static final int TOTAL 1000000;45 public static void main(String[] args) {6 Runnable r1 new Runnable() {7 Override8 public void run() {9 long startTime System.currentTimeMillis();10 for (int i 0; i TOTAL; i) {11 cache.a (i-99999)*(i99999);12 }13 long endTime System.currentTimeMillis(); // 结束时间毫秒14 long cost endTime - startTime; // 耗时毫秒1516 System.out.println(方法耗时1: cost 毫秒);17 }18 };1920 Runnable r2 new Runnable() {21 Override22 public void run() {23 long startTime System.currentTimeMillis();24 for (int i 0; i TOTAL; i) {25 cache.b (i-99999)*(i99999);26 }27 long endTime System.currentTimeMillis(); // 结束时间毫秒28 long cost endTime - startTime; // 耗时毫秒2930 System.out.println(方法耗时2: cost 毫秒);31 }32 };3334 Thread t1new Thread(r1);35 t1.start();3637 Thread t2new Thread(r2);38 t2.start();39 }40 }复制代码代码逻辑是两个线程并发修改两个变量这两个变量在同一个实例里边。输出结果是这样的Connected to the target VM, address: 127.0.0.1:58237, transport: socket方法耗时2: 19 毫秒方法耗时1: 22 毫秒Disconnected from the target VM, address: 127.0.0.1:58237, transport: socketProcess finished with exit code 0我们来修改代码加上很多无效的变量重新执行缓存类复制代码1 public class CacheB {2 volatile int a;3 long temp10;4 long temp20;5 long temp30;6 long temp40;7 long temp50;8 long temp60;9 long temp70;1011 volatile int b;12 }复制代码线程类复制代码1 public class Main {2 private static CacheB cache new CacheB();3 private static final int TOTAL 1000000;45 public static void main(String[] args) {6 Runnable r1 new Runnable() {7 Override8 public void run() {9 long startTime System.currentTimeMillis();10 for (int i 0; i TOTAL; i) {11 cache.a (i-99999)*(i99999);12 }13 long endTime System.currentTimeMillis(); // 结束时间毫秒14 long cost endTime - startTime; // 耗时毫秒1516 System.out.println(方法耗时1: cost 毫秒);17 }18 };1920 Runnable r2 new Runnable() {21 Override22 public void run() {23 long startTime System.currentTimeMillis();24 for (int i 0; i TOTAL; i) {25 cache.b (i-99999)*(i99999);26 }27 long endTime System.currentTimeMillis(); // 结束时间毫秒28 long cost endTime - startTime; // 耗时毫秒2930 System.out.println(方法耗时2: cost 毫秒);31 }32 };3334 Thread t1new Thread(r1);35 t1.start();3637 Thread t2new Thread(r2);38 t2.start();39 }40 }复制代码执行结果如下Connected to the target VM, address: 127.0.0.1:58389, transport: socket方法耗时1: 10 毫秒方法耗时2: 10 毫秒Disconnected from the target VM, address: 127.0.0.1:58389, transport: socketProcess finished with exit code 0是不是很神奇我们给一个对象加了很多无用的变量它居然变快了。而且性能还提升了不少。这个优化的核心思路就是通过强制指定内存相对位置将不相关的变量强制分配到不同的缓存行上让缓存行不会因为当前不使用的缓存而被强制失效。很多人也喜欢这样子写private volatile long value;private long p1, p2, p3, p4, p5, p6, p7;通过手动补齐剩余字节确保当前变量尽可能在一个缓存行上。但是这样子写代码就很不方便了(防盗连接本文首发自http://www.cnblogs.com/jilodream/ )我们要增加很多无意义的字段或者通过其它变量穿插起来。很容易被别人误改误删也影响代码最重要的阅读性。因此java在8及以上的版本增加了一个注解ContendedContended美[kənˈtend] 英[kəntend]v.竞争;认为;争夺这个注解既可以用在类上也可以用在变量上代码如下缓存类复制代码1 import jdk.internal.vm.annotation.Contended;23 /**4 * discription5 */6 public class CacheC {7 Contended8 volatile int a;910 Contended11 volatile int b;12 }复制代码线程执行类复制代码1 public class Main {2 private static CacheC cache new CacheC();3 private static final int TOTAL 1000000;45 public static void main(String[] args) {6 Runnable r1 new Runnable() {7 Override8 public void run() {9 long startTime System.currentTimeMillis();10 for (int i 0; i TOTAL; i) {11 cache.a (i-99999)*(i99999);12 }13 long endTime System.currentTimeMillis(); // 结束时间毫秒14 long cost endTime - startTime; // 耗时毫秒1516 System.out.println(方法耗时1: cost 毫秒);17 }18 };1920 Runnable r2 new Runnable() {21 Override22 public void run() {23 long startTime System.currentTimeMillis();24 for (int i 0; i TOTAL; i) {25 cache.b (i-99999)*(i99999);26 }27 long endTime System.currentTimeMillis(); // 结束时间毫秒28 long cost endTime - startTime; // 耗时毫秒2930 System.out.println(方法耗时2: cost 毫秒);31 }32 };3334 Thread t1new Thread(r1);35 t1.start();3637 Thread t2new Thread(r2);38 t2.start();39 }40 }复制代码同时我们要在jdk 启动时配上虚拟机参数-XX:-RestrictContended这个配置参数表示启用Contended注解同时IDEA等(防盗连接本文首发自http://www.cnblogs.com/jilodream/ )工具还会提示我们在配置中开启编译选项开关允许代码访问jdk内部/隐藏的api--add-exports java.base/jdk.internal.vm.annotationALL-UNNAMED使用注解后执行结果如下Connected to the target VM, address: 127.0.0.1:56688, transport: socket方法耗时2: 11 毫秒方法耗时1: 12 毫秒Disconnected from the target VM, address: 127.0.0.1:56688, transport: socket和手动补齐的速度差不多。手动补齐易于控制但是影响代码阅读交给虚拟机自动补齐。