程序问题引入

  1. /**
  2. * volatile 关键字,使一个变量在多个线程间可见
  3. * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
  4. * 使用volatile关键字,会让所有线程都会读到变量的修改值
  5. * <p>
  6. * 在下面的代码中,running是存在于堆内存的t对象中
  7. * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
  8. * 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
  9. * <p>
  10. * 使用volatile,将会强制所有线程都去堆内存中读取running的值
  11. * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
  12. *
  13. * @author mashibing
  14. */
  15. package com.mashibing.juc.c_001_01_Visibility;
  16. import com.mashibing.util.SleepHelper;
  17. import java.io.IOException;
  18. public class T01_HelloVolatile {
  19. private static volatile boolean running = true;
  20. // 没有volatile的时候主线程中对running的设置不起作用,而加了volatile之后会起作用
  21. // 牵扯到线程的可见性问题
  22. // private static /*volatile*/ boolean running = true;
  23. private static void m() {
  24. System.out.println("m start");
  25. while (running) {
  26. //System.out.println("hello");
  27. }
  28. System.out.println("m end!");
  29. }
  30. public static void main(String[] args) throws IOException {
  31. new Thread(T01_HelloVolatile::m, "t1").start();
  32. SleepHelper.sleepSeconds(1);
  33. running = false;
  34. System.in.read();
  35. }
  36. }

图解

  1. r位于主内存中,两个线程(t1会读、主线程隔一秒后会改)都会用到r
  2. t1读的是r的拷贝,t1将内存中的r拷贝一份放到线程本地(是线程缓存而不是ThreadLocal),每一次读的r并不是去主内存读,而是读的自己本地的缓存;主要缓存中的值没人改过,或者没有去主内存读,读到的都是之前的值,线程不会主动地去主内存中读(不hi是每次循环都要去主存中读一遍);每次读只是读的本地的
  3. 主线程1s后会将主线程本地缓存中的r改为false,但是由于t1不会从主内存中重新读,所以改了不会有影响,所以t1停止不了

image.png


线程可见性的基本概念(volatile)

  1. 一个线程改了值之后,另一个线程是不是能见到最新被修改的值
  2. 默认情况下,一个线程改了,另一个线程看不到
  3. 想要让他看见,只要让running值用volatile修饰


用volatile保障可见性

  1. volatile的第一个作用:保证线程的可见性
  2. 在上图中,volatile修饰的内存r,对于他的任何修改对其他线程立即可见===>线程读r时(每次要用到r时),每次都去主内存中去读一遍;而修改的线程修改之后会立马刷新到主内存中去(读的线程立马可见)
  3. MESI协议(不同cpu采用不同的缓存一致性协议,MESI只是缓存一致性协议中的一种)===> 只是类似,实际上与volatile没有什么联系(与volatile放一起就有点问题了)===>详见下面缓存行中没有volatile时的缓存一致性???
  4. 不是一定要volatile类型的数据才会产生问题
  5. 一个是程序角度上的同步volatile,一个是操作系统CPU角度上的缓存一致性协议???

某些其他语句触发内存缓存同步刷新(能做到与volatile相同的效果)

  1. 在上面代码中m方法的while循环中添加一个打印语句===>running变得可见了
  2. System.out.println(“hello”);触发了可见性机制
  3. System.out.println();方法中有一个synchronized,synchronized也是可以保证可见性的===>在某些语句执行的情况下,可以触发线程本地缓存与主内存之间的数据刷新和同步的
  4. 每个里面加sout?不要这样做,每加一个sout就相当于上了一把锁,效率会变低
  5. 不能用sout这种笨拙的方法保证可见性,该用volatile还是用volatile
  1. private static void m() {
  2. System.out.println("m start");
  3. while (running) {
  4. System.out.println("hello");
  5. }
  6. System.out.println("m end!");
  7. }

volatile修饰引用类型(包括数组)

  1. 只能保证引用本身的可见性,不能保证内部字段的可见性
  2. volatile修饰引用类型时,内部字段在线程的本地缓存中仍然是不可见的,修改内部字段不会同步刷新
  3. volatile修饰引用类型时,只有在该引用指向了另外一个对象时,线程本地中的引用也会同步刷新===>就是说这个引用变量是可见的,但是引用所指向的对象的内部字段仍然是不可见的;内部字段r仍然处在线程本地缓存中
  4. 想让内部字段在线程本地内存中可见,在内部字段前面加volatile,而不是在引用变量前面加volatile
  5. volatile修饰引用类型相对较少,一般只存在于面试题中(了解即可)
  6. 尽量避免使用volatile关键字修饰引用变量。
    1. ”双缓冲+引用赋值“的方法
    2. 加锁
  1. /**
  2. * volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
  3. */
  4. package com.mashibing.juc.c_001_01_Visibility;
  5. import com.mashibing.util.SleepHelper;
  6. public class T02_VolatileReference {
  7. private static class A {
  8. boolean running = true;
  9. void m() {
  10. System.out.println("m start");
  11. while (running) {
  12. }
  13. System.out.println("m end!");
  14. }
  15. }
  16. private volatile static A a = new A();
  17. public static void main(String[] args) {
  18. new Thread(a::m, "t1").start();
  19. SleepHelper.sleepSeconds(1);
  20. a.running = false;
  21. }
  22. }

CPU的三级缓存

cpu三级缓存存取周期对比

image.png

多核cpu中(三级)缓存的位置

  1. 机箱中有好几颗cpu,核中有好几个核(下图中有两颗cpu,每颗cpu中有两个核)
  2. L1缓存和L2缓存都位于核的内部,L3缓存在这颗cpu的内部
  3. 寄存器需要变量x,首先会去L1中找,如果L1没有回去L2找,如果L3没有会去L3找,如果L3没有会去主存中找;读数据时,首先往L3中存一份,再往L2中存一份,然后往L1中存一份,最后读到寄存器中去
  4. 上面说的线程的缓存的可见性主要是L1和L2中的x和其他核甚至其他cpu中读的同一个x的可见性(可以有好几层的可见性)===>而不是ThreadLocal

image.png

缓存行

  1. 不是每一个数据需要多长就将多长的数据放到各级缓存中,为了让内存(io)效率更高,读取的时候往往是按照一块一块来读的(一读就是一整块,时空局部性原理
  2. 按块读取===>一整块的读取到缓存中,这一块数据有多大===>一次性读取的数据叫做缓存行
  3. 程序局部性原理,可以提高效率
    1. 程序读数据的时候,相邻的数据很快就能读到(读一个值的时候,与他相邻的值也会很快就用到)===>空间局部性原理(读数据)
    2. 程序读指令的时候,很快相邻的指令也会用到(读指令的时候一次性将很多相邻的指令也读到缓存或者内存中)===>时间局部性原理(读指令)
  4. 充分发挥总线cpu针脚等一次性读取更多数据的能力
  5. cache line:在内存中任何数据的组织都是按照一行一行来组织(或者一块一块来组织),读的时候都是一整块一整块的读进来
  6. 缓存行的大小:64byte
    1. 64byte的原因:
      1. 缓存行越大,局部性空间效率越高(命中率高),但读取时间慢
      2. 缓存行越小,局部性空间效率越低(命中率低),但读取时间快
      3. 取一个折中值,目前多用:64字节
      4. 工业实践中得出的最佳实践
  7. 图解

image.png

缓存一致性协议(缓存行的应用)(骨灰级别的程序调优的写法)

  1. package com.mashibing.juc.c_001_02_FalseSharing;
  2. import java.util.concurrent.CountDownLatch;
  3. public class T01_CacheLinePadding {
  4. // 循环10亿次
  5. public static long COUNT = 10_0000_0000L;
  6. private static class T {
  7. //private long p1, p2, p3, p4, p5, p6, p7;
  8. // 一个long类型8byte===>一个缓存行64byte===>8个long填满一个缓存行=
  9. public long x = 0L; //8bytes
  10. //private long p9, p10, p11, p12, p13, p14, p15;
  11. }
  12. public static T[] arr = new T[2];
  13. static {
  14. arr[0] = new T();
  15. arr[1] = new T();
  16. }
  17. public static void main(String[] args) throws Exception {
  18. CountDownLatch latch = new CountDownLatch(2);
  19. Thread t1 = new Thread(() -> {
  20. for (long i = 0; i < COUNT; i++) {
  21. arr[0].x = i;
  22. }
  23. latch.countDown();
  24. });
  25. Thread t2 = new Thread(() -> {
  26. for (long i = 0; i < COUNT; i++) {
  27. arr[1].x = i;
  28. }
  29. latch.countDown();
  30. });
  31. final long start = System.nanoTime();
  32. t1.start();
  33. t2.start();
  34. latch.await();
  35. System.out.println((System.nanoTime() - start) / 100_0000);
  36. }
  37. }
  1. 将第10行与第13行的注释打开(即在T中x的前后各多定义7个long类型的变量)
  2. 发现用的时间提升了,变快了,效率变高了(700+ns—->200+ns)
  3. 上述代码与volatile无关,加不加volatile都会有效率上的区别
  4. 出现这种现象的原因:
    1. 当前后没有填充的时候,x和y大概率地是位于同一行(同一缓存行)
    2. 在上图中,因为x和y位于同一行,虽然左边的计算单元与寄存器单元只访问x,但是x和y会被全部缓存过来;右边的计算单元与寄存器只访问y,但是x和y也会全部缓存过来
    3. 既然一行数据同时在cpu1和cpu2中,必须采用某种机制保存两者的数据一致性===>数据一致性协议(一边的缓存行失效了或者被更改了,和另外一边的缓存行之间就要保持一致)
    4. 机制:一边修改了,就要通知另一边:我这边修改了或者失效了,你要重新从内存中刷新一遍新的数据过来
    5. 因此,有缓存一致性协议的存在就导致花了额外的时间,造成效率低
    6. 而加了前后各7个填充之后,两个数据x和y必然不处于同一行,每个cpu只需要读自己需要的那一行即可(这次读的包含x的行中不含有y,包含y的行中不含有x),这样就不需要做通知和同步,省了通知和同步的时间,提高了效率
    7. 缓存行(缓存一致性协议)与volatile没有什么特殊的联系
  5. MESR协议(缓存一致性协议)与volatile===> 只是类似,实际上与volatile没有什么联系(与volatile放一起就有点问题了)
  6. 不是一定要volatile类型的数据才会产生问题
  7. 一个是程序角度上的同步volatile,一个是操作系统CPU角度上的缓存一致性协议???
  8. volatile与缓存行的概念没有半毛钱的关系

上述缓存行对齐运用的实际程序

  1. jdk1.7的源码道格李的linked blocking queue类中运用了这种写法===>填充
  2. Disruptor框架===>闪电(速度快)(交易所的开源框架,得过计算机界杜克奖)
    1. 目前为止,效率最高的mq(单机版的)消息队列
    2. 消息缓存===>一般指字节数组(头尾指针、位置指针……)
    3. mq的缓存时环形的缓冲区(ring buffer),只有一个指针(在那转)
    4. 通过CAS和cache line padding(缓存行对齐)提高了效率
    5. 具体的内容之后会有

image.png image.png

Contended注解

  1. 手动添加变量不方便,而且可能面临缓存行大小的变化(目前为64byte,一行可以存放8个long类型的变量(每个8byte))
  2. 缓存行变化,填充的大小不用变===>oracle在jdk1.8时提供了一个注解,保证被他标注的数据不会和其他的数据为同一行
  3. 添加注解后,效率大幅度上升(700+ns—->200+ns)
  4. 要想让注解生效起作用,要在jvm运行行参数中添加参数:-XX:-RestrictContended(默认情况下,该注解是被限制住的,不会起作用,要用参数将其打开)上面的参数第二个横线为减号,表示去掉Contended限制(设为false的意思)
  5. 这个注解用的很少,写jdk内部类时会用(会的人也少);1.9之后就不起作用了,只有1.8起作用
  6. 尽量别这样用===>追求效率还没有达到这种骨灰级的程度(属于走火入魔了)===>以后哪一天写自己的SDK可能会用的上!
  1. package com.mashibing.juc.c_001_02_FalseSharing;
  2. import sun.misc.Contended;
  3. //注意:运行这个小程序的时候,需要加参数:-XX:-RestrictContended
  4. import java.util.concurrent.CountDownLatch;
  5. public class T05_Contended {
  6. public static long COUNT = 10_0000_0000L;
  7. // 要想让注解生效起作用,要在jvm运行行参数中添加参数:-XX:-RestrictContended
  8. @Contended //只有1.8起作用 , 保证x位于单独一行中
  9. private static class T {
  10. public long x = 0L;
  11. }
  12. public static T[] arr = new T[2];
  13. static {
  14. arr[0] = new T();
  15. arr[1] = new T();
  16. }
  17. public static void main(String[] args) throws Exception {
  18. CountDownLatch latch = new CountDownLatch(2);
  19. Thread t1 = new Thread(() -> {
  20. for (long i = 0; i < COUNT; i++) {
  21. arr[0].x = i;
  22. }
  23. latch.countDown();
  24. });
  25. Thread t2 = new Thread(() -> {
  26. for (long i = 0; i < COUNT; i++) {
  27. arr[1].x = i;
  28. }
  29. latch.countDown();
  30. });
  31. final long start = System.nanoTime();
  32. t1.start();
  33. t2.start();
  34. latch.await();
  35. System.out.println((System.nanoTime() - start) / 100_0000);
  36. }
  37. }

硬件层面上的缓存一致性

  1. 缓存一致性协议不要和MESI这个概念混淆,不同的cpu可能有不同的缓存一致性协议(MSI、MOSI、……),MESI是intel CPU设计的,所以更有名
  2. 主动性的更新数据,会主动监听,只要一边改了,另一边就会得到通知说那边改了,下次取的时候就会重新从内存中读一遍
  3. 这东西跟volatile没关系,任何一种类型的数据都会启动MESI协议
  4. 网上博客一般将volatile和MESI放一起讲,但是volatile底层不是由MESI实现的
  5. 这就是硬件层级的原理===>因为有这个协议的存在,才会有上面那种**通过缓存行对齐进行效率提升的方法**(因为要保证缓存一致性就会消耗时间,降低效率)

image.png

总线锁是一种重量级的锁,会进一步降低效率

总结

  1. volatile保障线程可见性(是线程本地缓存,不是ThreadLocal)
    1. 互相之间保持数据一致性而采取的一种机制
    2. 和内存中的某些数据保持同步
    3. volatile可以保障的线程的可见性
  2. 缓存行
    1. 缓存的概念(L1、L2、L3缓存)
    2. 读取时一行一行地读缓存行的内容===>诡异的编程技巧
  3. 缓存一致性协议
    1. 多数cpu底层支持缓存一致性协议
    2. 与加不加volatile没有关系
  4. volatile(保证可见性和有序性)底层实现的原理:详见有序性之后的volatile原理

附录

SleepHelper帮助类(只是简答地将try……catch……扔到帮助类中)

在演示式的代码中写很多try……catch……,看起来费劲

  1. package com.mashibing.util;
  2. import java.util.concurrent.TimeUnit;
  3. public class SleepHelper {
  4. public static void sleepSeconds(int seconds) {
  5. try {
  6. TimeUnit.SECONDS.sleep(seconds) ;
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. }