目录

1. JMM模型

2. 并发和并行

3.并发三大特性

 3.1 可见性

3.2 上下文切换

 3.3 lock指令的作用

4. java并发知识体系

5. 计算机组成架构

6. CPU三级缓存架构

7.缓存一致性机制

7.1 总线裁决 

 7.2 缓存行填充问题

8.java可见性保证 

8.1 JVM层面的内存屏障和硬件层面的内存屏障


1. JMM模型

JMM内存模型是java虚拟机提出的一种规范,用来协调操作系统与硬件的各种差异,实现并发效果。

其中有8种原子指令:

1.lock:锁定变量

2.unlock:解锁锁定的变量,当锁定多次时需要解锁同样次数,锁定与解锁必须成对出现

3. read,load:从主内存读取变量值加载到本地内存,两个原子指令必须顺序执行

4. use:将本地内存中的变量值给执行引擎,随时给线程使用

5. assign:将执行引擎的值赋值给本地内存

6. store,write:从本地内存存储变量值并写入到主内存,两个原子指令必须顺序执行

2. 并发和并行

并行:在同一时刻有多条指令执行

 

 并发:在同一时刻只能有一个指令执行,并伴随指令之间的切换时间很短。大概在10-100ms之间,用户无法感知。

并发解决问题:

        同步:一个任务依赖另外一个任务的执行完成才能执行。

        互斥:一个任务的资料不能被其他任务使用。

        分工:一个很大的任务量分解为更多小的任务最后进行重组得出答案。

 

3.并发三大特性

原子性:单个指令不可分割

可见性:对变量的写操作,在任务线程中都能看到最后写入的值

有序性:程序编写顺序和执行顺序一致,即使在多线程中也要保持此顺序一致

保证可见性的操作:

1.内存屏障:

volatile,内存屏障(jdk8中UnsafeFactory.getUnsafe().storeFence();调用内存屏障)

synchronized,lock,final

2. 上下文切换:Thread.yieled

实际上在底层C++都是调用汇编指令调用内存屏障保证,如果需要查看汇编指令可以在VM参数中加入

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp

如果缺少hsdis-amd64.dll文件需要下载可以在控制台打印汇编指令,通过搜索lock关键字。

 3.1 可见性

在一个线程变量可以被另外一个线程变量读取到最后的值。

可见性的几种方法:

1. volatile修饰

2.内存屏障

3.使用synchronized修饰

4.使用Lock保证可见性

5. 使用final关键字保证可见性

比较保守的保证可见性的方法是使用volatile关键字修饰

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.locks.LockSupport;

/**
 * 可见性测试 七种实现可见性的方法
 */
public class VolatileTest {

    private boolean flag = true;
//    private volatile boolean flag = true;//第一种方法使用volatile
    private int count = 0;
//    private volatile int count = 0;//第六种方法给count值声明volatile
//    private Integer count = 0;//第七种方法给count值声明Integer,底层使用final修饰保证可见性

    public void changgeFlag(){
        flag = false;
        System.out.println(Thread.currentThread().getName()+"flag:"+flag);
    }

    public void load() throws Exception{
        System.out.println(Thread.currentThread().getName()+"开始执行");
        while (flag){
            count++;

            //第二种方法内存屏障保证可见性,不建议使用
//            Unsafe.getUnsafe().storeFence();//报错:java.lang.SecurityException: Unsafe
            //1.最简单的使用方式是基于反射获取Unsafe实例
//            Field f = Unsafe.class.getDeclaredField("theUnsafe");
//            f.setAccessible(true);
//            Unsafe unsafe = (Unsafe) f.get(null);
//            unsafe.storeFence();

            //第三种方法 通过上下文切换读取主内存的值保证
//            Thread.yield();

            //第四种保证可见性的方法 使用synchronized底层使用内存屏障保证
//            System.out.println(count);

            //第五种保证可见性的方法 发放许可的方式
//            LockSupport.unpark(Thread.currentThread());

            //第八种方法等待一段时间把本地内存缓存清除从主内存拿值
//            waitLong(1000000);//1000ms
//            waitLong(1000);//1ms 时间太短不会跳出循环

            //第九种方法
            Thread.sleep(1000);
        }
        System.out.println(Thread.currentThread().getName()+"跳出循环:count="+count);
    }

    private static void waitLong(long interval){
        long begin = System.nanoTime();//纳秒值
        long end;
        do {
            end = System.nanoTime();
        }while(begin+interval > end);

    }


    public static void main(String[] args) throws Exception {
        //两个方法,第一个方法依靠flag执行死循环
        VolatileTest v1 = new VolatileTest();

        Thread thread = new Thread(()-> {
            try {
                v1.load();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"threadA");
        thread.start();
        //第二个对象改变flag值跳出死循环
        Thread.sleep(1000);
        Thread thread1 = new Thread(()->v1.changgeFlag(),"threadB");
        thread1.start();
    }
}

3.2 上下文切换

 上下文切换:一个线程执行一半被线程2抢占时间片,等线程2执行完以后再执行线程1。中间需要使用到PC-程序计数器指向到原来执行前的步骤。

 3.3 lock指令的作用

lock前缀指令的作用:将指令前的写指令全部执行完,将缓存中的值全部失效。所有的值只能从主内存中读取,达到可见性的目的

4. java并发知识体系

1.线程基础

2.线程安全:同步,互斥,分工

3. 并发工具类

4. 并发设计模式

5. 计算机组成架构

伪代码演示计算机计算过程:

x=3;

y=x+5;

1.PC->记录执行代码位置->比如y=x+5;这一行

2.load:registers->cache(未加载到X)->内存(X=3);->cache(x=3)->register;

3.add:ALU(y=3+5=8)->registers(y=8)->cache(y=8)->内存(y=8)

 

6. CPU三级缓存架构

CPU现在一般有三级缓存架构:两个L1Cache,一个L2 Cache,一个多核心共享的L3 Cache

为什么要引入三级缓存:内存存取周期比CPU要慢很多,如果中间有个缓存,里面有的值可以直接取,大大减少CPU的等待时间。

局部性原理:在CPU访问存储设备时,无论是存储数据或指令都趋向于聚集在一块连续的区域。分为两种:时间局部性和空间局部性。

时间局部性:一个信息被访问,那么在近期它可能还会被访问。

空间局部性:如果一个存储器的位置被引用,那么它附近的位置也会被引用。

 

 

7.缓存一致性机制

 计算机中为了解决缓存不一致的问题,有两种机制保证缓存一致性:窥探机制和基于目录的机制。

窥探机制(<=64核处理器使用):每个请求都必须广播到系统中的所有节点,速度很快,但是很耗带宽。

基于窥探机制有两种实现方式:write-invalidate(写失效),write-update(少用,考虑带宽问题)

write-invalidate(写失效):当一个处理器中的缓存值更改后会告知其他处理器的缓存值失效,保证只能有一个缓存值有效。常见的协议有MSI,MESI,MOSI,MOESI,MESIF

        MESI协议:M-修改 E-独占 S-共享 I-无效;

        伪代码演示X=5;X=3+X;协议过程如下:

        1. 第一个线程:内存read->load指令->CPU高速缓存(X=5),此时缓存处于E独占状态

        2. 第二个线程内存read->load指令->CPU高速缓存(X=5)->此时缓存处于S-共享状态

        3. 第一个线程内存->ALU(3+5=8=X)->CPU高速缓存(X=8)->此时缓存处于M-修改状态,其他处理器缓存处于I-无效状态

        4.lock指令会保证缓存立即刷回主内存(X=8)

write-update(写更新):当一个处理器中的缓存值更改后会更改其他处理器的缓存值,但要考虑带宽问题,用的比较少。

缓存一致性无法保证的两种场景:1.跨缓存行(比如X中的值大于64字节) 2.早期处理器没有实现缓存一致性协议,总线锁定,串行执行。

基于目录的机制(>64核处理器使用):延迟更长,带宽使用很少。

7.1 总线裁决 

当有多个处理器同时请求总线,总线会去判定其中一个处理器A胜出,处理器B只能等待A处理完成才能请求。

总线锁定:当处理器A执行请求时,其他处理器都不能使用,总线锁定是使用lock前缀指令和lock#信号实现

#linux服务器查看缓存块大小
cd /sys/devices/system/cpu/cpu0/cache/index0
ls
[root@localhost index0]# cat coherency_line_size

#或者输入以下命令查看cache_alignment参数:64
cat /proc/cpuinfo

 

 7.2 缓存行填充问题

缓存行默认只能存储64个字节,如果不够64位就可能使用volatile导致缓存一致性协议使其他缓存失效,导致只能不停从主内存读取,为了解决这个问题,有两种方法:

1.jdk7中第一种方法 避免伪共享: 缓存行填充7个long,每个long占用8个字节,加上x等于64个字节

2.第二种方法,避免伪共享: @Contended +  jvm参数:-XX:-RestrictContended  jdk8支持

import sun.misc.Contended;

/**
 * 伪共享
 */
public class FalseSharingTest {

    public static void main(String[] args) throws InterruptedException {
        testPointer(new Pointer());
    }

    private static void testPointer(Pointer pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 思考:x,y是线程安全的吗?
        System.out.println(pointer.x+","+pointer.y);

        System.out.println(System.currentTimeMillis() - start);


    }
}


class Pointer {
    // 2.第二种方法,避免伪共享: @Contended +  jvm参数:-XX:-RestrictContended  jdk8支持
    @Contended
     volatile long x;
    //1.jdk7中第一种方法 避免伪共享: 缓存行填充7个long,每个long占用8个字节,加上x等于64个字节
//    long p1, p2, p3, p4, p5, p6, p7;
     volatile long y;
}

 

8.java可见性保证 

1. 单线程程序不能保证内存可见性问题。java在编译时:java-class->java指令序列->汇编指令->机器码,在不影响最终结果的前提下会进行一个重排序,多线程无法保证。

正确使用同步的程序可以通过禁止编译器和处理器的重排序保证内存可见性。

JMM不能保证所有线程都能看到一致的操作执行顺序。

JMM不能保证在32位处理器对64位的long和double变量写操作的原子性。

未正确同步的多线程不能保证。

汇编指令级的内存屏障一共四种:LoadLoad,LoadStore,StoreStore,StoreLoad,但X86处理器只有一种stroeLoad可以生效,其他三种都为空操作。

JVM层级的内存屏障:UnSafeFactory.getUnSafe().storeFence();这种调用内存屏障的方式jdk8以后可能会被舍弃,一般使用volatile保证内存可见性即可。

volatile内存语义:

1.可见性:对一个volatile修饰的变量,总能在多个线程中看到最后写入的变量值。

2.有序性:通过对volatile修饰的变量读写操作前后加上各种特定的内存屏障来禁止指令重排。

3.原子性:不能保证原子性,面试不要说

8.1 JVM层面的内存屏障和硬件层面的内存屏障

JVM层面有4个内存屏障:写入STORELOAD,STORESTORE,读取LOADLOAD,LOADSTORE,但在x86处理器中只有STORELOAD有用,其他三个操作都是空操作。

STORELOAD:在load之前会将store之前的所有写入操作可见,兼具了其他三种的功能。

硬件层面的内存屏障有4个:lfence-读屏障,sfence-写屏障,mfence-读写屏障的功能都有,lock-非内存屏障,通过对总线和缓存加锁,达到内存屏障的功能。

内存屏障的功能:1.阻止屏障两边的指令重排序 2.使当前处理器中的缓存立即刷回到主内存。

 

 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐