MESI协议,JMM,线程常见方法等

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

[[329428]]

本文转载自微信公众号「 学习Java的小姐姐」,转载本文请联系 学习Java的小姐姐公众号。

前言

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

但是网上很多博文直接上来就讲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,其进入阻塞状态。

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

一些常见的线程操作

创建线程的三种方式

线程和任务合并

  1. Thread thread=new Thread(){       
  2.  public void run(){       
  3.      System.out.println("开始");   
  4.  }  
  5. }; 

线程和任务分开

  1.  Runnable runnable=new Runnable() {        
  2.       @Override            
  3.       public void run() {       
  4.          System.out.println("开始");            
  5.       }        
  6. };  
  7. Thread thread=new Thread(runnable); 

FutureTask返回执行结果

  1. FutureTask<String> futureTask=new FutureTask<String>(new Callable<String>() {           
  2.    @Override      
  3.         public String call() throws Exception {     
  4.              return "线程的返回值";       
  5.     }         
  6. });  
  7. Thread thread=new Thread(futureTask); 

线程启动start

  1. thread.start(); 

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

等待线程运行结束join

未加join情况:

  1.  Runnable runnable=new Runnable() {             
  2.  @Override            
  3.   public void run() {   
  4.         System.out.println("线程开始");       
  5.         try {         
  6.              sleep(4000L);            
  7.         } catch (InterruptedException e) {    
  8.              e.printStackTrace();               
  9.         }           
  10.          System.out.println("线程结束");     
  11.      }     
  12.  };  
  13. //创建线程  
  14. Thread thread=new Thread(runnable);  
  15. //启动线程 
  16.  System.out.println("主线程开始"); 
  17.  thread.start(); 
  18.  System.out.println("主线程结束"); 

运行结果:

使用join的情况:

  1. Runnable runnable=new Runnable() {         
  2. @Override   
  3.  public void run() {    
  4.         System.out.println("线程开始");     
  5.         try {         
  6.              sleep(4000L);         
  7.         } catch (InterruptedException e) {   
  8.               e.printStackTrace();      
  9.         }       
  10.         System.out.println("线程结束");        
  11.   }  
  12. };  
  13.  //创建线程    
  14. hread thread=new Thread(runnable);   
  15. //启动线程   
  16. System.out.println("主线程开始");   
  17. thread.start();   
  18. thread.join();   
  19. System.out.println("主线程结束"); 

运行结果:

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

获取线程id,name,priority

  1.  //创建线程  Thread thread=new Thread(){       
  2.   public void run(){      
  3.        System.out.println("线程开始");      
  4.    } 
  5. };   
  6. //启动线程  
  7. thread.start();   
  8. System.out.println("id:"+thread.getId());   
  9. System.out.println("name:"+thread.getName());   
  10. 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的小姐姐0618
相关推荐

2024-11-07 11:17:50

2020-08-23 11:52:10

Docker容器技术

2022-01-04 06:50:12

数据摘要方法

2010-01-14 16:48:29

交换机故障

2010-08-16 16:49:30

DIV CSS居中

2019-09-02 15:33:23

AI换脸人脸转换深度学习

2010-09-07 09:33:20

2010-09-08 12:54:42

2022-05-23 11:35:16

jiekou幂等性

2015-08-13 13:47:17

2010-07-29 10:22:38

2010-01-12 09:37:48

VB.NET调用IE

2024-04-16 11:46:51

C#Redis数据库

2009-12-04 12:31:24

2010-08-18 09:24:09

IE6兼容性

2021-01-14 16:14:06

Python爬虫代码

2009-12-16 08:57:45

2018-11-07 09:01:13

Tomcat部署方式

2019-07-08 08:11:42

物联网设备物联网安全物联网

2021-02-03 12:47:09

Spring Boot应用监控
点赞
收藏

51CTO技术栈公众号