走向人生巅峰的大路

走向人生巅峰的大路

在说这个之前,我想先说一下计算机的内存模型:

CPU在执行的时候,肯定要有数据,而数据在内存中放着呢,这里的内存就是计算机的物理内存,刚开始还好,但是随着技术的发展,CPU处理的速度越来越快,而从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,所说设计师,就在物理内存与CPU之间,加入了缓存的概念:也就是CPU在运行的时候,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

(说到这里,你应该能想到在高并发,使用多线程处理的时候,存在的问题。)

单核CPU只含有一套L1,L2,L3缓存;如果CPU含有多个核心,即多核CPU,则每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。

当你的计算式是:

单线程。cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。

单核CPU,多线程。进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。

多核CPU,多线程。每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

JMM内存模型

JMM全称为Java Memory Model  java内存模型,它只是一组规范,并不真实存在,(看好了,只是一组规范、一个定义),它描述的是一个规则。通过这组规范定义程序中的变量的访问方式。以此来解决多核CPU多线程时造成的问题(请想一想为什么会是多核CPU多线程时)。

JMM规定了工作内存、主内存,而主内存是共享区域,所有线程都可以访问,工作内存是每个线程的工作内存,每个线程对变量的操作必须在自己的工作内存中进行。在运行时将变量从主内存中拷贝到工作内存中,对变量进行操作,操作完后再将变量写会主内存,不能直接在主内存中操作变量。再说一遍:这是规范,是java定义的一组程序运行时的规范。看到这,是不是发现与计算机的内存模型特别像

JAVA内存区域

在这里再说一下java的内存区域:堆、栈、方法区、本地方法区、程序计数器

方法区:线程共享区域,主要用于存储虚拟机加载的类信息、常量、静态变量等数据。

堆:线程共享区域,虚拟机启动时创建,主要用于存放对象的实例,所以也是java垃圾回收最频繁的一个区域

栈:线程私有区域,与线程同时创建,栈数量与线程数量相等,以栈帧定义,执行每个方法时,都会创建一个栈帧存储方法的信息:操作数栈、动态链接方法、返回值、返回地址等信息,每个方法执行从调用到结束,对应着这个栈帧的入栈出栈。

程序计数器、本地方法栈我们先不关心,有心者请自行学习。

如果你看到这,我觉得你应该会疑惑,说JMM的时候,会什么要把计算机的内存模型、JAVA的内存区域都描述一下,稍后你就会知道。

在这里我还要再强调一遍,jmm内存模型的主内存和工作内存与java内存区域的堆、栈、方法区等不是同一个层次的,无法类比。一个是规范、规则,就相当于校规似的。如果真要对应,那就如同你想的  栈---工作内存,堆、方法区---主内存。

现在我要将这些联系起来了:

首先jmm是一组规范,是java提出来的规范,那么java在设计的时候,肯定也符合这组规范。我认为java的内存区域就是根据jmm规范设计的,所以上面说了,他们不属于一个层次,无法类比,只能说是java内存区域,符合jmm的规范。

然后我们也直达,线程是cpu调度的最小单元,也就是说cpu的内核执行线程,上面也说了,执行方法,也就是一个栈帧入栈出栈的过程,那么cpu内核执行线程,就是操作的栈中的数据,那么就是cpu内核把栈中的数据拷贝到它的缓存中去运行。可能有点模糊,我认为你就记住,cpu执行时的数据就是栈中的数据。

你可能在想,那堆中的数据时怎么操作的?我认为是这样:看到这,我也认为电脑面前的你知道了堆在栈中存的是一个地址,那么cpu内核在运行时,不也是根据这个地址找到那个数据了嘛,对cpu来讲,你就是一份数据,在它面前你跟栈中的数据都是一样的,都是数据,然后加到它的缓存中。

如果你读的有点蒙,那就请再读几遍,书读百遍,其义自见:也就是说你在读第一遍的时候,你根本就没理解,你的脑子里只有读过的字,并没有理解到写的含义,打个比方说:我说筷子,你脑海的潜意识中是筷子两根棍的样子,而不是“筷子”这两个字。

然后,请回顾上面提到的两个问题,我也在这贴出来一段代码,以此来表示多核多线程产生的问题:

public class VolatileFaceThread{
    boolean isRunning = true;
    void m() {
        System.out.println("isRunning start");
        while(isRunning) {     
        }
        System.out.println("isRunning end");
    }
    public static void main(String[] args) {
        VolatileFaceThread vft = new VolatileFaceThread();
        new Thread(vft :: m).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        vft.isRunning = false;
        System.out.println("update isRunning...");
    }
}

预期效果:新启线程会一直循环下去

这段代码,新启的线程将会一直循环下去,不会被停止。试验此段代码时,如果有的实际效果是在主线程修改后,新启的线程也跟着停止了,那么你的电脑可能1核在运行。(当初我在这被卡了好几天,让同事运行时,它运行的实际效果就是预期效果)。

这就是因为,两个线程被两个内核运行,他们把值读取到自己的缓存中运行。而缓存是每个内核私有的,主线程修改了值,对新启线程来说是不可见的,故新启线程会一直循环。

那怎么解决呢,就是让线程可见呗,java中有这一个关键字:volatile-----内存可见性、禁止指令重排序。

被volatile关键字修饰的变量对所有线程总是可见的,也就是在一个线程修改了一个被volatile关键字修饰变量的值,新值总是可以被其他线程立即得知。 

上面说了那么多,就是为了这个结论做铺垫,如果你不明白计算机的内存模型、jmm、java的内存区域,就算说了这个结论,我估计你也应该是一个糊里糊涂的脸,所以,请认真对待自己的疑问。它可以让你坎坷成长,也可以让你退缩安逸。

下次有时间再写一下volatile禁止重排序的功能。

参考博客:https://www.jianshu.com/p/8420ade6ff76

https://blog.csdn.net/javazejian/article/details/72772461

10-24 00:54