提升--03---并发编程之---可见性

Posted 高高for 循环

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提升--03---并发编程之---可见性相关的知识,希望对你有一定的参考价值。

并发编程三大特性

可见性

先看一个小案例:

import java.io.IOException;

public class T01_HelloVolatile {
    private static boolean running = true;

    private static void m() {
        System.out.println("m start");
        while (running) {

        }
        System.out.println("m end!");
    }

    public static void main(String[] args) throws IOException {

        new Thread(T01_HelloVolatile::m, "t1").start();

        SleepHelper.sleepSeconds(1);

        running = false;

        System.in.read();
    }
}

一直运行,没有结束…

分析:

  1. 在下面的代码中,running是存在于堆内存的t对象中
  2. 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy(缓存)并不会每次都去读取堆内存.
  3. 这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行.

使用 volatile 关键字:

volatile 关键字,使一个变量在多个线程间可见

  1. 使用volatile,将会强制所有线程都去堆内存中读取running的值,会让所有线程都会读到变量的修改值.
  2. volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized

注意:

volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性

public class T02_VolatileReference {

    private static class A {
        boolean running = true;

        void m() {
            System.out.println("m start");
            while (running) {
            }
            System.out.println("m end!");
        }
    }

    private volatile static A a = new A();

    public static void main(String[] args) {
        new Thread(a::m, "t1").start();
        SleepHelper.sleepSeconds(1);
        a.running = false;
    }


}

  1. 以上volatile 只能保证a对象修改时,各线程可见.但不能保证内部属性running 的可见性.
  2. 因为a对象是引用类型,地址值并未改变,只是内部属性running 被主线程改变,所以对t1线程来说,还是存在a对象内部属性running ,修改的不可见性.

三级缓存


- 寄存器Register读取数据是先从L1找,L1找不到 ,再L2,L3,再找不到,再从内存中去寻找

缓存行

程序局部性原理

缓存是按块,一行一行局部读取的

缓存行对齐

缓存行: 64个字节是CPU同步的基本单位

缓存行隔离会比伪共享效率要高Disruptor

认识缓存行对齐的编程技巧----案例1

需求:

  • 对一个数组内的元素,多线程的进行10亿次数据赋值.并计算这个共需要花费多少时间.

方式一:

数组中的两个元素arr[0],arr[1],大概率在同一个缓存行里

多线程操作数据时,因为为了保证多个cpu中的数据一致性,操作系统底层会通知各个cpu,当数据被修改时,需要从内存中读取跟新后的数据.从而达到各个cpu线程中的数据一致.

import java.util.concurrent.CountDownLatch;

public class T01_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class T {
     //   private long p1, p2, p3, p4, p5, p6, p7;
        public long x = 0L;
       // private long p9, p10, p11, p12, p13, p14, p15;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }

            latch.countDown();
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }

            latch.countDown();
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

方式二:

往对象T中加入属性,使得整个T对象的大小,大于64个字节.

所以数组中的两个元素arr[0],arr[1]一定不在同一个缓存行里.

import java.util.concurrent.CountDownLatch;

public class T01_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class T {
        private long p1, p2, p3, p4, p5, p6, p7;
        public  long x = 0L;
        private long p9, p10, p11, p12, p13, p14, p15;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }

            latch.countDown();
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }

            latch.countDown();
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

因为此时各cpu中的缓存行里的数据,不会被不同的线程修改,从而出现各cpu数据不一致的情况.

所以操作系统底层不需要频繁的通知各cpu线程,去从内存读取数据去跟新.

省略了通知和同步的时间,所以耗时更小,效率更高

@Contended注解

  • JDK8引入了@sun.misc.Contended注解,来保证缓存行隔离效果
  • 要使用此注解,必须去掉限制参数:-XX:-RestrictContended

只有1.8起作用 , 保证x位于单独一行中

缓存的一致性协议:

MESI是硬件层面的缓存一致性协议,Inter设计的

Disruptor 源码



以上是关于提升--03---并发编程之---可见性的主要内容,如果未能解决你的问题,请参考以下文章

并发编程之原子性可见性有序性的简单理解

[Java并发编程实战] 共享对象之可见性

[Java并发编程实战] 共享对象之可见性

提升--05---并发编程之---原子性---CAS

并发基础之Java内存模型JMM

JUC并发编程 共享模型之内存 -- Java 内存模型 & 原子性 & 可见性(可见性问题及解决 & 可见性 VS 原子性 & volatile(易变关键字))(代码