[并发编程基础篇]MESI协议,JMM,线程常见方法等

目录

前言

缓存一致性——MESI协议

CPU多级缓存官方概念

CPU多级缓存白话翻译

为什么需要CPU缓存

缓存的意义

带来的问题

解决方式一——总线加锁(性能太低)

解决方式二——MESI协议(重点)

并行和并发的区别

进程和线程的关系

线程之间的通信

进程之间的通信

线程的状态(从硬件层面)

一些常见的线程操作

创建线程的三种方式

线程启动start

等待线程运行结束join

获取线程id,name,priority

乱序优化

Java内存模型——JMM

内存模型

8种原子操作(概念)

8种原子操作(举例)

并发编程三大特性之可见性volatile

并发编程三大特性之原子性Synchronized

并发编程三大特性之有序性volatile

并发的风险与优势

结语

参考资料


 

前言

我们在找工作时,经常在招聘信息上看到有这么一条:要求多线程并发经验。无论是初级程序员,中级程序员,高级程序员,也无论是大厂,小厂,并发编程肯定是少不了的。

但是网上很多博文直接上来就讲JUC,没有从基础出发,所以该篇旨在讲明并发基础,主要为计算机原理,线程常见方法,Java虚拟机方法的知识,为后面的学习保驾护航,话不多说,开始吧。

缓存一致性——MESI协议

CPU多级缓存官方概念

CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU,所以才引入了缓存的概念。我们可以从下图看出在CPU和主内存之间加了一个缓存,用来提升交互速度。

随着CPU的速率越来越快,人们对计算机性能要求越来越高,传统的缓存已经满足不了,所以引入了多级缓存,包括一级缓存,二级缓存,三级缓存,具体如图所示。

一级缓存:基本上都是内置在cpu内部,和cpu一个速度运行,能有效的提升cpu的工作效率。当然数量越多,cpu工作效率就会越高,但是由于cpu的内部结构限制了其大小,所以一级缓存的数据并不大。

二级缓存:主要作用是协调一级缓存和内存之间的工作效率。cpu首先用的是一级内存,当cpu的速度慢慢提升之后,一级缓存就不够cpu的使用量了,这就需要用到二级内存。

三级缓存:和一级缓存与二级缓存的关系差不多,是为了在读取二级缓存不够用的时候而设计的一种缓存手段,在有三级缓存cpu之中,只有大约百分之五的数据需要在内存中调取使用,这能提升cpu不少的效率,从而cpu能够高速的工作。

我们可以看下本机的缓存情况。

CPU多级缓存白话翻译

只有一级缓存情况:

我们可以将CPU当做我们本人,缓存区当做超市,主内存当做工厂,如果想要买东西(取数据)就先去超市(缓存区)买(取),如果超市(缓存区)没有,就去工厂(主内存)里面买(取)。

多级缓存情况:

我们可以将CPU当做本人,一级缓存当做楼下小区里面的小卖部,二级缓存当做普通超市,三级缓存当做大型超市,主内存当做工厂,如果想买东西先去楼下小卖部(一级缓存),小卖部(一级缓存)没有的话,就去普通超市(二级缓存),如果普通超市(二级缓存)还没有,就去大型超市(三级缓存),如果大型超市(三级缓存)还没有,就直接去工厂(主内存)取。这些缓存的出现使得我们不必每次都去工厂(主内存)买东西(取数据),节省了时间,提升了速度。

为什么需要CPU缓存

CPU速率太快,快到内存跟不上,在处理器处理周期内,CPU常常等待内存,造成资源的浪费。

缓存的意义

  • 时间局限性:如果某个数据被访问,在将来的某个时间也可能被访问。(白话翻译就是如果我今天买了薯片,那么以后我可能还会买薯片,毕竟是吃货O(∩_∩)O)
  • 空间局限性:如果某个数据被访问,那么他相邻的数据也有可能被访问。(白话翻译就是如果我今天买了薯片,那么我可以还会买其他膨化食品,毕竟他们两挨在一起)

带来的问题

对于多核系统来说, 每个核中缓存数据不一致的问题。

解决方式一——总线加锁(性能太低)

CPU从主内存读取数据到缓存区,并在总线对这个数据进行加锁,其他CPU无法去读写这个数据,直到这个CPU使用完数据,锁被释放了才访问。就比如我想去超市买一个辣条,但是张三也想买,在我买的过程中,就给辣条加了锁,张三根本碰不到辣条,我买的过程非常慢,那张三不急死啦嘛。

解决方式二——MESI协议(重点)

针对上面缓存数据不一致的情况,提出了MESI协议用以保证多个CPU缓存中共享数据的一致性,定义了缓存行Cache Line四个状态,分表是M(Modified),E(Exclusive),S(Share),I(Invalid)四种。

  • M(Modified修改):该行数据有效,数据被修改了,和内存中的数据不一致,数据只能存在于本缓冲区中。
  • E(Exclusive独占):这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。
  • S(Shared共享):这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。
  • I(Invalid无效):这行数据无效

 

MESI状态之间的迁移

这图一看是很懵逼的,咱慢慢来看哈,慢慢体会这些变化哈。

当前状态是Modified

  • 内核读取本地缓存中的值(local read):从缓存区中读取数据,状态不变,还是修改M
  • 本地内核写本地缓存中的值(local write):从缓存区中修改数据,状态不变,还是修改M
  • 其它内核读取其他缓存中的值(remote read):数据被写入内存,其他内存读取到最新数据,即为共享S
  • 其它内核更改其他缓存中的值(remote write):数据被写入内存,其他内存读取到最新数据,并修改和提交,此缓存区的状态为无效I

 

当前状态是Exclusive

  • 内核读取本地缓存中的值(local read):从缓存区中读取数据,状态不变,还是独占E
  • 本地内核写本地缓存中的值(local write):从缓存区中修改数据,即为修改M
  • 其它内核读取其他缓存中的值(remote read):数据被写入内存,其他内存读取到数据,即为共享S
  • 其它内核更改其他缓存中的值(remote write):数据被写入内存,其他内存读取到数据,并修改提交,即为无效I

 

当前状态是Share

  • 内核读取本地缓存中的值(local read):从缓存区中读取数据,状态不变,还是共享S
  • 本地内核写本地缓存中的值(local write):在缓存区中修改数据,即为修改M
  • 其它内核读取其他缓存中的值(remote read):数据被写入内存,其他内存读取数据,即为共享S
  • 其它内核更改其他缓存中的值(remote write):数据被写入内存,其他内存读取数据,并修改提交,即为无效I

 

当前状态是Invalid

  • 内核读取本地缓存中的值(local read):如果其他缓存里面没有这个值,状态即为独享E;如果其他缓存里有这个值,状态即为共享S
  • 本地内核写本地缓存中的值(local write):在缓存区中修改数据,即为修改M
  • 其它内核读取其他缓存中的值(remote read):其他核的操作与他无关,即为无效I
  • 其它内核更改其他缓存中的值(remote write):其他核的操作与他无关,即为无效I

并行和并发的区别

并发:同一时刻只能有一个指令执行,但多个指令被CPU轮换执行,因为时间间隔很短,会造成同时执行的错觉。

并行:同一时刻多条指令在多个处理器同时执行,不管是微观,还是宏观上,都是同时执行的。

举个例子,并发就是一个家庭主妇既要烧饭,也要带娃,也要打扫房间,如果每个事情只做一分钟,然后轮换,从宏观上来说,会造成同时执行的错觉。并行就是该家庭主妇请了两个保姆,一个专职负责烧饭,一个专职负责带娃,自己专职负责打扫卫生,不管从宏观还是微观上来看,他们都是同时执行的。

某位大佬曾经说两者的区别,并发是同一时间应对多件事情的能力,并行是同一时间去多件事情的能力。作为一个工科生,不知道如何夸大佬,只知道喊666。

进程和线程的关系

进程是用来加载指令,管理内存,执行语句的。

线程是进程的一部分,一个进程可以分为1个或多个线程。

网易云音乐的打开,就是开启了一个进程,而播放,查找,评论等都是线程。

线程之间的通信

线程之间的通信比较简单,可以通过他们的共享内存通信,具体可以看下面Java内存模式部分。

进程之间的通信

进程之间的通信比较复杂,对于同一台计算机而言,其通信称为IPC;对于不同计算机,其通信需要网络并遵循彼此约定的协议,如HTTP等。这部分偏硬件,咱也不敢说,咱也不敢问。

线程的状态(从硬件层面)

初始状态:新建new一个线程,还没有进行任何步骤,还未和硬件关联上。

可运行状态:当调用start方法,即进行可运行状态(就绪状态),但是这个时候还没获取到时间片,具体什么时候运行取决于硬件。

运行状态:当CPU分配的时间片到某个线程了,该线程即可进入运行状态。

阻塞状态:当线程调用阻塞API,线程并没有用到CPU,其进入阻塞状态。

终止状态:当一个线程运行结束了,即进入终止状态。

一些常见的线程操作

创建线程的三种方式

线程和任务合并

Thread thread=new Thread(){
      public void run(){
          System.out.println("开始");
      }
};

线程和任务分开

 Runnable runnable=new Runnable() {
            @Override
            public void run() {
                System.out.println("开始");

            }
        };

 Thread thread=new Thread(runnable);

FutureTask返回执行结果

FutureTask<String> futureTask=new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "线程的返回值";
            }
        });

Thread thread=new Thread(futureTask);

线程启动start

thread.start();

这里start是进入就绪状态,即可运行状态,具体什么时候要看CPU。

等待线程运行结束join

未加join情况:

 Runnable runnable=new Runnable() {
            @Override
            public void run() {
                System.out.println("线程开始");
                try {
                    sleep(4000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程结束");
            }
        };
//创建线程
Thread thread=new Thread(runnable);
//启动线程
System.out.println("主线程开始");
thread.start();
System.out.println("主线程结束");

运行结果:

 

使用join的情况:

 Runnable runnable=new Runnable() {
       @Override
       public void run() {
           System.out.println("线程开始");
           try {
                sleep(4000L);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
           System.out.println("线程结束");
       }
 };
  //创建线程
  Thread thread=new Thread(runnable);
  //启动线程
  System.out.println("主线程开始");
  thread.start();
  thread.join();
  System.out.println("主线程结束");

 

运行结果:

没有用join方法的第一情况,主线程开始和主线程结束都在前面,并靠在一起,而线程开始和线程结束则在后面,因为他们是两个不同的线程,彼此互不干扰。而用了join方法的第二种情况,主线程结束在最后一行,因为join方法需要等待子线程结束后才能继续执行后面代码。

获取线程id,name,priority

 //创建线程
 Thread thread=new Thread(){
       public void run(){
           System.out.println("线程开始");
       }
 };
 //启动线程
 thread.start();

 System.out.println("id:"+thread.getId());
 System.out.println("name:"+thread.getName());
 System.out.println("priority:"+thread.getPriority());

运行结果:

乱序优化

咱先来看一个概念,重排序,也就是语句的执行顺序会被重新安排。其主要分为三种:

1.编译器优化的重排序:可以重新安排语句的执行顺序。

2.指令级并行的重排序:现代处理器采用指令级并行技术,将多条指令重叠执行。

3.内存系统的重排序:由于处理器使用缓存和读写缓冲区,所以看上去可能是乱序的。

上面代码中的a = new A();可能被被JVM分解成如下代码:

// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址复制代码

 

// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象  复制代码

一旦假设发生了这样的重排序,比如线程A在执行了步骤1和步骤3,但是步骤2还没有执行完。这个时候线程B有进入了第一个if语句,它会判断a不为空,即直接返回了a。其实这是一个未初始化完成的a,即会出现问题。

Java内存模型——JMM

内存模型

跟多级缓存差不多意思,每个线程里面都有工作内存,其存储的是主内存中数据的副本,如下图。那如果主内存中有变量a=1,现在线程A,B,C都存了a=1的副本,线程A对其进行加1操作,并刷新到主内存。可是线程B,C并不知道这种情况,那么就出问题啦。那如何解决这个问题呢?下面将慢慢说,不急。

8种原子操作(概念)

下面罗列的是8种原子操作,大家大概看看,下面将详细描述。

  • read(读取):从主内存中读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • user(使用):从工作内存读取数据来计算
  • assign(赋值):将计算好的值重新赋值到工作内存中
  • store(存储):将工作内存数据写入主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定):将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

8种原子操作(举例)

咱以上面的例子画了个图,请原谅偶我笨,画的丑了点。

1.read读取:将主内存中的a=1读取出来。

2.load载入:将从主内存中a=1载入到线程A的工作内存中。

3.use使用:将线程A工作内存的a=1读取到,并进行自增操作。

4.assign赋值:将a=2写入到线程A的工作内存中。

5.store存储:将a=2存储到主内存中。

6.write写入:将a=2写入到主内存的a变量中。

7.lock锁定:在上面CPU缓存解决不一致的方法一中,线程A操作的时候,对主内存a变量进行加锁操作(lock),线程B根本读不了a变量。

8.unlock解锁:线程A操作解锁之后,对主内存a变量进行解锁操作(unlock),线程B可以读到a变量并对其操作。

注意:lock和unlock存在着一个性能问题,我们发现写的代码明明是多线程并发操作,但是底层还是串行化,并没有真正实现并发。

并发编程三大特性之可见性volatile

上面说的MESI协议是在总线那边实践的,线程A,B可以同时获取主内存a的值,a进行自增操作之后在进行操作6write写入的时候,会经过总线。线程B一直使用嗅探监控总线中自己感兴趣的变量a,一旦发现a值有修改,like将自己工作内存中a置为无效Invalid(利用MESI协议),并立刻从主内存中读取a值,这个时候总线中a还没有写入内存,所以有个短暂的lock过程,等到a写入内存了,进行unlock操作,线程B即可读取新的a值。

该过程虽然也有lock与unlock操作,但是锁的粒度降低啦。

并发编程三大特性之原子性Synchronized

这个之后会出一篇详细文章。

并发编程三大特性之有序性volatile

这个上面乱序优化已经提到,就不写了,后面将出一篇他的详细文章。

并发的风险与优势

优势:

  • 速度方面:同时处理多个请求,响应更快,复杂的操作可以分成多个进程同时进行。
  • 设计方面:程序设计在某些情况下更简单,也可以有更多的选择。
  • 资源利用方面:CPU能够在等待IO的时候做一些其他的事情。

风险:

  • 安全性方面:多个线程共享数据时可能会产生与期望不相符的结果。
  • 活跃性方面:某个操作无法继续进行下去时,就会发生活跃性问题,比如死锁,节等问题。
  • 性能方面:线程过多时会使得:CPU频繁切换,调度时间增多;同步机制;消耗过多内存。

结语

看到这里的都是真爱,先行谢过。此篇是并发系列的基础,主要聊了硬件的MESI协议,原子的八种操作,线程和进程的关系,线程的一些基础操作,JMM的基础等。如果有什么错误,或者不对的地方,欢迎指正。

参考资料

Java并发编程入门与高并发面试

CPU多级缓存与缓存一致性

Java高并发编程精髓Java内存模型JMM详解全集

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值