专栏首页陈琛的Redis文章[并发基础篇]MESI协议,JMM,线程常见方法等

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

前言

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

但是网上很多博文直接上来就讲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());

运行结果:

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存在着一个性能问题,我们发现写的代码明明是多线程并发操作,但是底层还是串行化,并没有真正实现并发。

可见性原理

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

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

并发的风险与优势

优势:

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

风险:

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

结语

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

参考资料

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

CPU多级缓存与缓存一致性

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

本文分享自微信公众号 - 学习Java的小姐姐(huangtest01),作者:学习Java的小姐姐0618

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java中的Object类 (上篇)

    Object中的hashCode方法就是根据一定的规则与对象相关的信息映射成一个数值,这个数值称为散列值。

    陈琛
  • [万字长文,建议收藏]关于Synchronized锁升级,你该了解这些

    毫无疑问,synchronized是我们用过的第一个并发关键字,很多博文都在讲解这个技术。不过大多数讲解还停留在对synchronized的使用层面,其底层的很...

    陈琛
  • Docker系列——2.Docker的下载与安装

    Docker是一个容器平台,将应用程序和依赖打包在一起,在虚拟容器中独立运行,这样也就完全独立于环境。传统虚拟机方式运行 10 个不同的应用就要起 10 个虚拟...

    陈琛
  • Akka 指南 之「为什么现代系统需要新的编程模型?」

    几十年前,卡尔·休伊特(Carl Hewitt)提出了 Actor 模型,将其作为在高性能网络中处理并行任务的一种方法——当时还没有这种环境。如今,硬件和基础设...

    CG国斌
  • Java Thread wait、notify与notifyAll

    Tencent JCoder
  • Linux实时补丁即将合并进Linux 5.3

    Linux PREEMPT_RT 补丁终于要合并进Linux 5.3了。意味着开发了十几年的实时补丁将得以和主线Linux 协同发展。

    jeff xie
  • Linux实时补丁即将合并进Linux 5.3

    所谓实时,就是一个特定任务的执行时间必须是确定的,可预测的,并且在任何情况下都能保证任务的时限(最大执行时间限制)。实时又分软实时和硬实时,所谓软实时,就是对任...

    Linux阅码场
  • 【Java面试宝典】深入理解JAVA虚拟机

      Java虚拟机管理的内存包括几个运行时数据内存:方法区、虚拟机栈、本地方法栈、堆、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区...

    郭耀华
  • 初学者第68节多线程之线程池(十一)

    线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运...

    用户5224393
  • Ingo Molnar 的实时补丁

    Ingo Molnar 的实时补丁是完全开源的,它采用的实时实现技术完全类似于Timesys Linux,而且中断线程化的代码是基于TimeSys Linux的...

    233333

扫码关注云+社区

领取腾讯云代金券