Java 并发与高并发知识学习

简介:   一、并发与高并发基本概念  并发:  从业务上简单解释就是多个用户(编码层面就是多个线程)共同竞争(修改或读取)一个资源,并发问题更多体现在业务代码操作数据上,例如:秒杀场景,瞬间会有大量用户共同抢购一个商品,这时候如果没有并发控制,则极有可能出现超卖情况,即库存被扣成了负数。  从操作系统以及硬件层面解释并发:有多个线程运行在CPU上,当在单核处理上运行的时候,多个线程在单核处理上交替执行(伪并行),不断的从内存中换入换出,在多核处理器上每个线程会被分配到某一个内核上运行(并行),我觉得更适合叫并行计算。

  一、并发与高并发基本概念

  并发:

  从业务上简单解释就是多个用户(编码层面就是多个线程)共同竞争(修改或读取)一个资源,并发问题更多体现在业务代码操作数据上,例如:秒杀场景,瞬间会有大量用户共同抢购一个商品,这时候如果没有并发控制,则极有可能出现超卖情况,即库存被扣成了负数。

  从操作系统以及硬件层面解释并发:有多个线程运行在CPU上,当在单核处理上运行的时候,多个线程在单核处理上交替执行(伪并行),不断的从内存中换入换出,在多核处理器上每个线程会被分配到某一个内核上运行(并行),我觉得更适合叫并行计算。

  高并发(High Concurrency):

  高并发更多是指系统级别的解决方案,解决方案中会包含并发相关的业务代码,同样是秒杀场景,根据用户量级对程序设计、数据库设计、硬件布局等等综合起来用于满足高并发场景(也可以理解为支持更多的并行计算)。

  二、并发安全的代码演示(基本演示)

  public class CountDownLatchExample1 {

  // 模拟并发2000

  private final static int threadCount=2000;

  // 模拟有100000个请求

  private final static int threadClient=100000;

  // 计数器(资源)

  private static int count=0;

  public static void main(String[] args) throws InterruptedException {

  // 线程池

  ExecutorService executor=Executors.newCachedThreadPool();

  // 信号量,用于模拟并发

  final Semaphore semaphore=new Semaphore(threadCount);

  for (int i=0; i < threadClient; i++) {

  executor.execute(new Runnable() {

  public void run() {

  try {

  // 获取一个资源

  semaphore.acquire();

  add();

  // 释放一个资源

  semaphore.release();

  } catch (InterruptedException e) {

  e.printStackTrace();

  }

  }

  });

  }

  executor.shutdown();

  System.out.println(count);

  }

  // 加了同步关键字的方法,如果不加同步则最终会因为并发问题导致结果不固定

  public synchronized static void add(){

  count++;

  }

  }

  三、CPU 多级缓存、缓存一致性、乱序执行优化

  1、CPU 多级缓存,如一下两张图,是简化的结构图

  \

  左图为早期的缓存结构示意图,右图为后来演变的多级缓存结构示意图

  为什么需要CPU缓存呢?因为CPU处理速度太快了,主存跟不上CPU的处理速度,在CPU处理时钟周期内经常会等待,浪费处理器资源,所以在主存与CPU之间增加了Cache,用于增加处理器利用率。

  缓存的意义:

  a、时间局部性:如果某个数据被访问,那么将来它可能还会被访问。

  b、空间局部性:如果某个数据被访问,那么与它相邻的数据也可能会被访问。

  由于缓存容量远远小于主存容量,所以缓存的数据也会有不命中的情况,即使这样也比直接访问主存要性能高。

  通过右图看到,后来发展到多级缓存,级数越高缓存的数据内容越多,极大的提高了处理器利用率,越靠近CPU的缓存,使用频率越高,数据是从主存>Ln>L2>L1 这样被写入,CPU访问的时候则是L1>L2>Ln>主存

  2、缓存一致性

  缓存一致性有个专有名词,叫 MESI (Modified Exclusive Shared Or Invalid),这个协议为了保证多个CPU(或CPU内核)之间缓存共享数据的一致性定义了Cache line 的四种状态,被修改的,独享的,共享的,无效的。

  a、被修改的(Modified)

  该状态只被缓存在该CPU的缓存中,并且是被修改过,与主存中的数据不一致,该缓存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

  b、独享的(Exclusive)

  该状态只被缓存在当前CPU缓存中,未被修改过的并且与主存中的数据一致,该状态下可以被其它CPU读取,从而变成共享状态,该状态可以变更为 Modified

  c、共享的(Shared)

  该状态,缓存可能存在于多个CPU 中,并且与主存中的数据一致,当某个CPU修改了数据后,其它CPU的缓存可以变为 Invalid 状态

  d、无效的(Invalid)

  该状态,当前CPU的缓存可能已经失效了,被二手游戏购买其它CPU修改过,并且已经写回主存

  MESI 状态转换图:

  \

  local read 读本地缓存

  local write 写本地缓存

  remote read 读主存数据

  remote write 写主存数据

  在一个典型系统中,可能会有几个缓存(在多核系统中,每个核心都会有自己的缓存)共享主存总线,每个相应的CPU会发出读写请求,而缓存的目的是为了减少CPU读写共享主存的次数。

  一个缓存除在Invalid状态外都可以满足cpu的读请求,一个invalid的缓存行必须从主存中读取(变成S或者 E状态)来满足该CPU的读请求。

  一个写请求只有在该缓存行是M或者E状态时才能被执行,如果缓存行处于S状态,必须先将其它缓存中该缓存行变成Invalid状态(也既是不允许不同CPU同时修改同一缓存行,即使修改该缓存行中不同位置的数据也不允许)。该操作经常作用广播的方式来完成。

  缓存可以随时将一个非M状态的缓存行作废,或者变成Invalid状态,而一个M状态的缓存行必须先被写回主存。

  一个处于M状态的缓存行必须时刻监听所有试图读该缓存行相对应的主存操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S状态之前被延迟执行。

  一个处于S状态的缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。

  一个处于E状态的缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S状态。

  对于M和E状态而言数据总是精确的,他们在和该缓存行的真正状态是一致的。而S状态可能是非一致的,如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

  从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。

  MESI 转换关系表

  \

  MESI 原文(自备梯子):

  https://en.wikipedia.org/wiki/MESI_protocol

  3、乱序执行优化

  乱序执行优化解释:处理器为了提高运行速度而做出的一些违背代码原有执行顺序的优化。

  例如:

  (1) int a=10;

  (2) int b=20;

  (3) int c=a+b;

  (4) System.out.println(c);

  上述代码中本意的执行顺序是1->2->3->4,但是CPU为了提高效率,可能是 2->1->3->4 这样执行的。

  这样的优化对于我们编写的程序来说在一些场景下不进行特殊处理,可能会产生与预期不符的结果。

  例如(例子可能不对,欢迎读者指正):

  private static boolean flag=false;

  public static void main(String[] args) throws InterruptedException {

  // 执行一个线程(想先输出false)

  new Thread(new Runnable() {

  public void run() {

  System.out.println(flag);

  }}).start();

  // 改变值

  flag=!flag;

  }

  四、JAVA 内存模型(JMM)、同步操作与规则

  1、JMM 简介 (Java Memory Model)

  为了屏蔽各种硬件、系统之间访问内存的差异,以及让 JAVA 程序在各个平台上的并发处理保持一致,JVM(Java虚拟机) 中规定了JAVA内存模型,它规范了Java 虚拟机与内存是如何协同工作的,规定了一个线程如何以及何时可以看到其它线程修改过的共享变量的值,以及线程在必须时如何同步的访问共享变量。

  a) JVM 内存分配简述

  \

  绿色区为栈,蓝色区为堆

  堆为JAVA程序运行时的数据区域,堆是动态分配内存的(运行时分配),由于是运行时分配内存,所以存取效率上会有所损耗,堆也是GC回收的主要区域。

  栈为JAVA程序运行时存放代码、原始类型的区域,栈的存取效率要高的多,仅次于CPU寄存器,栈的数据可以共享,栈中的数据大小以及生命周期必须是确定的,栈中存储的是JAVA当中的基本类型(byte,char,short,int,long,float,double,boolean)以及对象句柄(引用)。

  JVM内存模型规定,调用栈与本地变量存放到线程栈上(Thread Stack),对象本身还是存在堆上(如图所示 Object 3),一个对象可能包含方法,这些方法中的本地变量也可能指向的是对象,这个对象同样存放在堆上,非对象类型的的本地变量都会存在于栈中。

  静态成员变量跟随着类的定义也被存放到堆上,存放在堆上的对象可以被持有这个对象的线程访问,同时持有这个对象的线程可以访问这个对象中的成员变量,如果两个线程同时拥有一个对象的访问权,则两个线程都可以访问这个对象中的成员变量,但是对这个成员变量的访问都会在各自线程中生成私有拷贝,当两个线程都修改了各自的私有拷贝时,都会写回主存,在没有做特殊处理的时候执行结果往往都不是预期结果。

  JMM 与硬件之间的关联关系(左图JMM,右图硬件结构)

  \

  从图中可以看到,在硬件层面上是不区分线程栈与堆的,而JMM中的栈、堆则可能分布在主存、高速缓存或者寄存器中。

  JMM 抽象结构图(非计算机物理结构)

  \

  JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

  2、JMM规定的八种操作以及同步规则

  lock 将主内存中的变量锁定,标记为一个线程所独占

  unclock 将主内存中之前lock的变量解除锁定,此时其它的线程可以有机会访问此变量

  read 将主内存中的变量值读到工作内存当中

  load 将read读取的值保存到工作内存中的变量副本中。

  use 将工作内存中的值传递给线程的代码执行引擎

  assign 将执行引擎处理返回的值重新赋值给工作内存中的变量副本

  store 将工作内存中变量副本的值存储到主内存中。

  write 将store存储的值写入到主内存的共享变量当中。

  规则一、

  如果把一个变量从主存加载到工作内存中,必须按顺序执行read-load ,如果把工作内存中的变量写回主存,则必须按照顺序执行 store-write ,虽然说必须按照顺序执行,但是没有说明是必须连续执行,所以read/store 后可能会中断然后执行其它处理后再执行 load/write 。

  规则二、

  不允许 read&load 或 store&write 的单独执行,例如 read 后必须有load 操作,store 必须有 write,反之,load 前必须先read,write 必须先有store。

  规则三、

  不允许线程丢弃它最近的 assign 操作,既变量在工作内存改变后必须写回主存,如果没有发生 assign 则不允许执行 store-write 操作。

  规则四、

  一个新变量只能从主存中诞生,不允许工作线程使用未被 read-load 或 assign 的变量,既,在use前必须 read-load,在store 前必须 assign。

  规则五、

  一个变量在同一时刻只允许一个线程对其执行lock 操作,但lock可以反复被同一个线程执行多次,有多少次lock,则有多少次unlock,这样才能保证变量被解锁,lock 与 unlock 必须成对出现

  规则六、

  如果对一个变量进行lock 操作,则会清空该线程工作内存中对应的变量副本,在执行引擎使用这个变量前要重新进行 load 或 assgin 初始化变量

  规则七、

  如果一个变量事先没有lock ,则不能对其进行unlock 操作,也不允许unlock 其它线程lock的变量

  规则八、

  一个线程在执行unlock 之前必须先执行 store-write 操作,把数据同步回主存。

  图形描述

  \

  五、volatile 的特殊性

  Java 内存模型对 volatile 做了特殊规定,当一个变量被定义为了 volatile 后会具备两种特性。

  a、保证变量对所有线程可见,volatile变量的写操作除了对它本身的读操作可见外,volatile写操作之前的所有共享变量对volatile读操作之后的操作可见。

  b、禁止指令重排序

  这里要注意一点,volatile 不保证原子性,例如 count++,count +=1 这样的操作在多线程下计算结果是不确定的。

  六、并发的优势与风险

  优势:

  a、处理速度,比如同时处理多个请求或者大任务拆分成小任务进行并行处理

  b、提高资源利用率,CPU可以在等待IO(磁盘、网络等)的时候去操作别的事情,充分发挥多核处理性能

  风险:

  a、安全性,主要体现在多个线程操作共享资源,没有合理使用加锁,则会造成未知结果

  b、活跃性,某个线程无法继续执行下去时就容易产生该问题,例如多个线程竞争资源可能会造成死锁问题,或者由于编码不当导致死锁

  c、性能,过多的线程会造成CPU频繁调度,返回会降低处理效率,而且线程过多会消耗更多的内存,因此要合理使用线程,比如使用线程池

目录
相关文章
|
19天前
|
XML Java 编译器
Java学习十六—掌握注解:让编程更简单
Java 注解(Annotation)是一种特殊的语法结构,可以在代码中嵌入元数据。它们不直接影响代码的运行,但可以通过工具和框架提供额外的信息,帮助在编译、部署或运行时进行处理。
85 43
Java学习十六—掌握注解:让编程更简单
|
5天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
4天前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
27天前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
26 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
10天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
27 2
|
12天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
14天前
|
缓存 监控 Java
Java 线程池在高并发场景下有哪些优势和潜在问题?
Java 线程池在高并发场景下有哪些优势和潜在问题?
|
27天前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
25 1
|
27天前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
35 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
29天前
|
前端开发 Java 应用服务中间件
Javaweb学习
【10月更文挑战第1天】Javaweb学习
30 2
下一篇
无影云桌面