java并发问题_并发理论基础:并发问题产生的三大根源

并发问题变幻莫测,一谈到并发就显得非常高深,一般的程序员对于并发问题也是头疼不已,但是随着网络互联越来越普遍,大规模用户访问网站程序也越来越频繁,并发问题又无法避免。

在我们解决并发问题前首先要理解产生并发问题的根源是什么,所有并发处理的工具只是针对这些根源问题的其中一种解决方案,如果只去了解解决方案而不理解问题的根源是什么,那么我们就很难正确的定位问题并对症下药。所以要写好并发程序我们首先就要深入理解并发问题产生根源是什么?

起因:如何最大化的利用CPU

CPU运算速度和IO速度的不平衡一直是计算机优化的一个课题,我们都知道CPU运算速度要以百倍千倍程度快于IO的速度,而在进行任务的执行的时候往往都会需要进行数据的IO,正因为这种速度上的差异,所以当CPU和IO一起协作的时候就产生问题了,CPU执行速度非常快,一个任务执行时候大部分时间都是在等待IO工作完成,在等待IO的过程中CPU是无法进行其它工作的,所以这样就使得CPU的资源根本无法合理的运用起来。

CPU就相当于我们计算机的大脑,如何把CPU资源合理的利用起来就直接关系到我们计算机的效率和性能,所以为了这个课题计算机分别从缓存、任务切换、指令排序优化这几个方向进行了优化 。

一、进程和线程的产生

在最原始的系统里计算机内存中只能允许运行一个程序,这个时候的CPU的能力完全是过剩的,因为CPU在接收到一个任务之后绝大部分时间都是处在IO等待中,CPU根本就利用不起来,所以这个时候就需要一种同时运行多个程序的方法,这样的话当CPU执行一个任务IO等待的时候可以切换到另外一个任务上去执行指令,不必在IO上浪费时间,那么CPU就能很大程度的利用起来,所以基于这种思路就产生了进程和线程。

有了进程后,一个内存可以划分出不同的内存区域分别由多个进程管理,当一个进程IO阻塞的时候可以切换到另外一个进程执行指令,为了合理公平的把CPU分配到各个进程,CPU把自己的时间分为若干个单位的片段,每在一个进程上执行完一个单位的时间就切换到另外一个进程上去执行指令,这就是CPU的时间片概念。有了进程后我们的电脑就可以同时运行多个程序了,我们可以一边看着电影一边聊天,在CPU的利用率又进一步提升了CPU的利用率。

因为进程做任务切换需要切换内存映射地址,而一个进程创建的所有线程,都是共享一个内存空间的,所以线程做任务切换成本就很低了,现代的操作系统都基于更轻量的线程来调度,现在我们提到的“任务切换”都是指“线程切换”。

并发问题根源之一:CPU切换线程执导致的原子性问题

首先我们先理解什么叫原子性,原子性就指是把一个操作或者多个操作视为一个整体,在执行的过程不能被中断的特性叫原子性。

因为IO、内存、CPU缓存他们的操作速度有着巨大的差距,假如CPU需要把CPU缓存里的一个变量写入到磁盘里面,CPU可以马上发出一条对应的指令,但是指令发出后的很长时间CPU都在等待IO的结束,而在这个等待的过程中CPU是空闲的。

所以为了提升CPU的利用率,操作系统就有了进程和时间片的概念,同一个进程里的所有线程都共享一个内存空间,CPU每执行一个时间段就会切换到另外一个进程处理指令,而这执行的时间长度是是以时间片(比如每个时间片为1毫秒)为单位的,通过这种方式让CPU切换着不同的进程执行,让CPU更好的利用起来,同时也让我们不同的进程可以同时运行,我们可以一边操作word文档,一边用QQ聊天。

后来操作系统又在CPU切换进程执行的基础上做了进一步的优化,以更细的维度“线程”来切换任务执行,更加提高了CPU的利用率。但正是这种CPU可以在不同线程中切换执行的方式会使得我们程序执行的过程中产生原行性问题。

比如说我们以一个变量赋值为例:

语句1:Int number=0;

语句2:number=number+1;

在执行语句2的时候,我们的直觉number=number+1 是一个不可分割的整体,但是实际CPU操作过程中并非如此,我们的编译器会把number=number+1 拆分成多个指令交给CPU执行。

number=number+1的指令可能如下:

指令1:CPU把number从内存拷贝到CPU缓存。

指令2:把number进行+1的操作。

指令3:把number回写到内存。

在这个时候如果有多线程同时去操作number变量,就很有可能出现问题,因为CPU会在执行上面任何一个指令的时候切换线程执行指令,这个时候就可能出现执行结果与我们预期结果不符合的情况。

比如如果现在有两个线程都在执行number=number+1,结果CPU执行流程可能会如下:

执行细节:

1、CPU先执行线程A的执行,把number=0拷贝到CUP寄存器。

2、然后CPU切换到线程B执行指令。

3、线程B 把number=0拷贝到CUP寄存器。

4、线程B 执行number=number+1 操作得到number=1。

5、线程B把number执行结果回写到缓存里面。

6、然后CPU切换到线程A执行指令。

7、线程A执行number=number+1 操作得到numbe=1。

8、线程A把number执行结果回写到缓存里面。

9、最后内存里面number的值为1。

二、高速缓存的产生

为了减少CPU等待IO的时间,让CPU有更多的时间是花在运算上,最简单的思路就是减少IO等待的时间,基于这个思路所以就有了高速缓存增加了高速缓存(L1,L2,L3,主存)。

在计算机系统中,CPU高速缓存是用于减少处理器访问内存所需的时间,其容量远小于内存,但其访问速度却是内存IO的几十上百倍。当处理器发出内存访问请求时,会先查看高速缓存内是否有请求数据。如果存在(命中),则不需要访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。

并发问题根源之二:缓存导致的可见性问题

在有了高速缓存之后,CPU的执行操作数据的过程会是这样的,CPU首先会从内存把数据拷贝到CPU缓存区。

然后CPU再对缓存里面的数据进行更新等操作,最后CPU把缓存区里面的数据更新到内存。

磁盘、内存、CPU缓存会按如下形式协作。

缓存导致的可见性问题就是指我们在操作CPU缓存过程中,由于多个CPU缓存之间独立不可见的特性,导致共享变量的操作结果无法预期。

在单核CPU时代,因为只有一个核心控制器,所以只会有一个CPU缓存区,这时各个线程访问的CPU缓存也都是同一个,在这种情况一个线程把共享变量更新到CPU缓存后另外一个线程是可以马上看见的,因为他们操作的是同一个缓存,所以他们操作后的结果不存在可见性问题。

而随着CPU的发展,CPU逐渐发展成了多核,CPU可以同时使用多个核心控制器执行线程任务,当然CPU处理同时处理线程任务的速度也越来越快了,但随之也产生了一个问题,多核CPU每个核心控制器工作的时候都会有自己独立的CPU缓存,每个核心控制器都执行任务的时候都是操作的自己的CPU缓存,CPU1与CPU2它们之间的缓存是相互不可见的。

这种情况下多个线程操作共享变量就因为缓存不可见而带来问题,多线程的情况下线程并不一定是在同一个CUP上执行,它们如果同时操作一个共享变量,但因为在不同的CPU执行所以他们只能查看和更新自己CPU缓存里的变量值,线程各自的执行结果对于别的线程来说是不可见的,所以在并发的情况下会因为这种缓存不可见的情况会导致问题出现。

比如下面的程序:

两个线程同时调用addNumber() 方法对number属性进行+1 ,循环10W次,等两个线程执行结束后,我们的预期结果number的值应该是20000,可是我们在多核CPU的环境下执行结果并非我们预期的值。

public class TestCase {

private int number=0;

public void addNumber(){

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

number=number+1;

}

}

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

TestCase testCase=new TestCase();

Thread threadA=new Thread(new Runnable() {

@Override

public void run() {

testCase.addNumber();

}

});

Thread threadB=new Thread(new Runnable() {

@Override

public void run() {

testCase.addNumber();

}

});

threadA.start();

threadB.start();

threadA.join();

threadB.join();

System.out.println("number="+testCase.number);

}

}

打印结果:

三、指令优化

进程和线程本质上是增加并行的任务数量来提升CPU的利用率,缓存是通过把IO时间减少来提升CPU的利用率,而指令顺序优化的初衷的初衷就是想通过调整CPU指令的执行顺序和异步化的操作来提升CPU执行指令任务的效率。

指令顺序优化可能发生在编译、CPU指令执行、缓存优化几个阶,其优化原则就是只要能保证重排序后不影响单线程的运行结果,那么就允许指令重排序的发生。其重排序的大体逻辑就是优先把CPU比较耗时的指令放到最先执行,然后在这些指令执行的空余时间来执行其他指令,就像我们做菜的时候会把熟的最慢的菜最先开始煮,然后在这个菜熟的时间段去做其它的菜,通过这种方式减少CPU的等待,更好的利用CPU的资源。

并发问题根源之三:指令优化导致的重排序问题

下面的程序代码如果init()方法的代码经过了指令重排序后,两个方法在两个不同的线程里面调用就可能出现问题。

private static int value;

private static boolean flag;

public static void init(){

value=8; //语句1 flag=true; //语句2 }

public static void getValue(){

if(flag){

System.out.println(value);

}

}

根据上面代码,如果程序代码运行都是按顺序的,那么getValue() 中打印的value值必定是等于8的,不过如果init()方法经过了指令重排序,那么结果就不一定了。根据重排序原则,init()方法进行指令重排序重排序后并不会影响其运行结果,因为语句1和语句2之间没有依赖关系。 所以进行重排序后代码执行顺序可能如下。

flag=true; //语句2 value=8; //语句1

如果init()方法经过了指令重排序后,这个时候两个线程分别调用 init()和getValue()方法,那么就有可能出现下图的情况,导致最终打印出来的value数据等于0。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/354989.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[luoguP1849] [USACO12MAR]拖拉机Tractor(spfa)

传送门 神奇的spfa #include <queue> #include <cstdio> #include <cstring> #include <iostream> #define N 1010 #define max(x, y) ((x) > (y) ? (x) : (y))int n, mx, my; int dis[N][N]; bool map[N][N], vis[N][N]; int dx[4] {0, -1, 0, 1…

在Eclipse上创建JSF / CDI Maven项目

当我在研究JSF和CDI示例时&#xff0c;我认为提及创建JSF和CDI Maven项目所需的步骤会很有用。 您可以找到以下步骤。 工具类 默认情况下&#xff0c;M2E插件随附的Eclipse Luna。 因此&#xff0c;无需自己安装插件。 WildFlye8.x。 从主菜单中选择文件->新建->其他。…

luoguP3690 【模板】Link Cut Tree (动态树)[LCT]

题目背景 动态树 题目描述 给定&#xff2e;个点以及每个点的权值&#xff0c;要你处理接下来的&#xff2d;个操作。操作有&#xff14;种。操作从&#xff10;到&#xff13;编号。点从&#xff11;到&#xff2e;编号。 &#xff10;&#xff1a;后接两个整数&#xff08;&a…

python爬虫多进程_Python爬虫技术--基础篇--多进程

要让Python程序实现多进程(multiprocessing)&#xff0c;我们先了解操作系统的相关知识。Unix/Linux操作系统提供了一个fork()系统调用&#xff0c;它非常特殊。普通的函数调用&#xff0c;调用一次&#xff0c;返回一次&#xff0c;但是fork()调用一次&#xff0c;返回两次&am…

java 电力系统_算法java实现--动态规划--电路布线问题

/** dianlubuxian.java* Version 1.0.0* Created on 2017年11月30日* Copyright ReYo.Cn*/package reyo.sdk.utils.test.dy;/*** 创 建 人&#xff1a;AdministratorReyoAut * 创建时间&#xff1a;2017年11月30日 下午4:58:56** author ReYo* version 1.0*//*** 电路布线问题(…

百度图片网址

http://qcloud.dpfile.com/pc/jPAgaVMWC7zueHYEzky7IUJs0w6QIgvTQ0p08wxCK1OUDUk6-KqvLg70OVUXtjEHTYGVDmosZWTLal1WbWRW3A.jpg转载于:https://www.cnblogs.com/leshen/p/7387677.html

antlr idea 入门_ANTLR:入门

antlr idea 入门这篇文章使您了解ANTLR的基础知识。 以前&#xff0c;我们已经了解了如何将ANTLR设置为外部工具。 在这里&#xff1a; ANTLR外部工具 :) 所以&#xff0c;我们开始…。 什么是ANTLR&#xff1f; •另一个语言识别工具&#xff0c;是一种语言工具&#xff0c;它…

typescript主键自增长

常见的不重复id创建方式有两种&#xff0c;一个是搞一个自增长数列&#xff0c;另一个是采用随机生成一组不可能重复的字符序列&#xff0c;常见的就是UUID了。我们来引入一个uuid的包&#xff1a;npm i --save angular2-uuid&#xff0c;由于这个包中已经含有了用于typescript…

java api操作hbase_通过JavaAPI使用HBase

1.准备工作(1) 启动zookeeper服务&#xff0c;我的是在本地启动zookeeper/usr/local/zookeeper/bin$ sudo zkServer.sh start(2) 启动HBase和HBase shell启动HBase:/usr/local/hbase/bin下启动start-hbase.sh启动HBase shell/usr/local/hbase/bin下终端输入hbase shell(3) 工程…

SPOJ QTREE5 lct

题目链接 对于每一个节点&#xff0c;记录这个节点所在链的信息&#xff1a; ls:&#xff08;链的上端点&#xff09;距离链内部近期的白点距离 rs:&#xff08;链的下端点&#xff09;距离链内部近期的白点距离 注意以上都是实边 虚边的信息用一个set维护。 set维护的是…

Java EE 8 MVC:使用路径参数

在上一篇文章中&#xff0c;我们看到了如何在Java EE MVC中使用查询参数 。 这篇文章继续与一个非常相似的主题&#xff1a;路径参数。 路径参数是请求路径的动态部分&#xff0c;可以使用Path注释指定。 例如&#xff1a; Controller Path("path-params") public…

duilib入门简明教程 -- 部分bug (11) (转)

原文转自&#xff1a;http://www.cnblogs.com/Alberl/p/3344886.html 一、WindowImplBase的bug在第8个教程【2013 duilib入门简明教程 -- 完整的自绘标题栏(8)】中&#xff0c;可以发现窗口最大化之后有两个问题&#xff0c;1、最大化按钮的样式还是没变&#xff0c;正确的样式…

在考生文件夹存有JAVA3_注意:下面出现的“考生文件夹”均为%USER%在考生文件夹下存有文件名为J_网考网(Netkao.com)...

【分析解答题】注意&#xff1a;下面出现的“考生文件夹”均为%USER%在考生文件夹下存有文件名为Java_2.java文件&#xff0c;本题功能是完成点定义&#xff0c;并输出点坐标。请完善Java_2.java文件&#xff0e;并进行调试&#xff0c;使程序结果如下&#xff1a;x5 y5点的坐标…

jasperreports_JasperReports JSF插件用例系列

jasperreports这是文章系列的切入点&#xff0c;在该系列文章中&#xff0c;我将尝试介绍JasperReport JSF插件的一些用例&#xff0c;该工具的创建是为了轻松地将为JasperReports设计的业务报告集成到JSF应用程序中。 该系列中描述的所有示例都可以从JasperReports JSF插件网站…

RN 47 中的 JS 线程及 RunLoop

RCBridge 初始化时声明了一个 CADisplayLink _jsDisplayLink [CADisplayLink displayLinkWithTarget:self selector:selector(_jsThreadUpdate:)];在 _jsThreadUpdate 函数中&#xff0c;处理界面更新。这个 CADisplayLink 随后被加到 JS 线程对应的 RunLoop 中。 - (void)ad…

java nginx https_docker nginx 配置ssl,实现https

docker nginx 配置ssl&#xff0c;实现https2019-09-05 16:06:35.0nginx配置https总览在nginx配置ssl实现https&#xff0c;简单来说分为三个步骤&#xff1a;1 上传ssl证书等文件将 1_www.domain.com_bundle.crt 和 2_www.domain.com.key 上传到nginx配置文件的目录旁边。这两…

Java EE 8 MVC:使用表单参数

在前两篇文章中&#xff0c;我们了解了如何在即将到来的Java EE MVC框架中使用查询和路径参数 。 这篇文章重点介绍表单参数。 当您使用发布请求提交Web表单时&#xff0c;表单值将作为请求正文的一部分发送。 媒体类型&#xff08;或内容类型&#xff09;定义了用于在请求正文…

Elasticsearch索引自动删除

简介 脚本分2部分&#xff0c;1部分查找符合条件的索引名&#xff0c;2脚本调用1脚本&#xff0c;进行删除操作 脚本 查找符合条件的&#xff0c;默认大于30天 # coding:utf-8__author__ Jipu FANGfrom elasticsearch import Elasticsearch import re import time import dat…

JavaScript入门几个概念

JavaScript入门几个概念 刚刚入门JavaScript的时候&#xff0c;搞懂DOM、BOM以及它们的对象document和window很有必要。 DOM是为了操作文档出现的API&#xff0c;document是它的一个对象。BOM是为了操作浏览器出现的API&#xff0c;window是它的一个对象。DOM When a web page …

idea中使用osgi_OSGi环境中的Servlet基本身份验证

idea中使用osgi您首先需要获得对OSGI HTTP Service的引用。 您可以通过声明性服务来做到这一点。 这篇文章将集中在获得对HTTP服务的引用之后的步骤。 注意&#xff1a;此职位的完整课程位于此处 通过OSGI HTTP Service注册Servlet时&#xff0c;它为您提供了提供HTTPContext实…