Java并发原理

简介: 网上有不计其数的并发编程文章,甚至有不计其数的书来介绍这个主题。你为什么要花10分钟时间来读完这篇文章呢?我给的答案:“他们全是废话。”,我觉得这个主题用10分钟就可以说完,根本不要用花这么长时间,也不用去折腾Java内存模型之类的东西。

网上有不计其数的并发编程文章,甚至有不计其数的书来介绍这个主题。你为什么要花10分钟时间来读完这篇文章呢?我给的答案:“他们全是废话。”,我觉得这个主题用10分钟就可以说完,根本不要用花这么长时间,也不用去折腾Java内存模型之类的东西。

我只讲原理,不会告诉你怎么用Java的并发库,这是java doc干的事情

理解Java并发原理或者其他语言的并发(没错,这篇文章是“跨语言”的!!!还这么短,你说牛逼不牛逼)只需要记住理解两个东西:

CPU访问存储的方式——多级存储;

CPU执行指令的方式——乱序

首先回忆我们大学的一门课程——《计算机组成原理》也许你的记忆里只有:“呃,你要说xx进制转换成xx进制吗?”。没关系我帮你回忆一下:

有一节课讲多级存储,说计算机最快的存储是CPU里面的Cache,其次是内存,最后是硬盘,最次的是外部存储(比如光盘之类的)。

还有一节课讲的是CPU流水线,乱序执行、分支预测,说CPU考虑性能问题会把几个没有数据关联的指令打乱顺序执行。

怎么样?有印象了吗?(什么?没读过大学?那我觉得你有必要读一下大学的课程——即便你不想混文凭)。

多级存储

我们来看一个“无聊的”Java例子(例子没有任何意义,会枯燥一些,耐着性质你读懂了可以超脱了)

img_c241d5b7e75d4da1ddb5f2b371ddf623.jpe

程序定义了一个线程,线程会不停的判断stop标志位,如果为真则循环累加i。然后我们在主线程里面修改stop为true。期望线程在进行2秒之后停止。

如果运行这个程序我们得到的结果是——程序永远不会停止。主线程里面修改的变量在testThread里面并没有发生改变。

解释这个程序就用到了“多级存储”,在x86架构的CPU中对数据的的访问都是经过寄存器,如果数据在内存中CPU会先加载到寄存器然后在读取;写入的时候CPU只写入到寄存器,在“适当的时候”数据会被回写到内存中。画个图:

img_75c17654c406bd8c2ca4d9a3b5a3e0b6.jpe

操作系统把我们程序中的主进程和testThread调度到不同的CPU,testThread(CPU1)访问stop的时候数据被复制到Cache中然后读取;主进程(CPU2)访问stop的时候数据被复制到Cache中然后读取,赋值的时候会写入到Cache中。所以CPU2修改的值并不会立马被CPU1看到,这取决于:

CPU2是不是写回到内存中;

CPU1的Cache是不是被“淘汰”重新从内存中加载数据;

第一条比较容易满足,因为Cache必定会回写到内存中(只不过不是实时写入);第二条看起来比较困难,唯一的解决办法是我们访问stop变量的时候每次都从内存加载而不是通过Cache。在Java中实现这个功能的关键字是volatile。

public static volatile boolean stop = false;

这样程序就可以“正常”执行了。需要注意,volatile只保证“好吧,我不用Cache”,无法保证原子性(比如赋值操作被拆分为多个CPU指令,那么其他进程可能看到的是一个“中间结果”)。所以volatile其实是一种低效、不安全的并发处理方式。(不使用Cache效率低,无法保证原子性所以不安全)。

流水线,乱序执行、分支预测

代码比上一个更加枯燥,忍耐一下:

img_a8145163f1cec514909794fa0b7ac5d5.jpe

我定义了4个变量,两个线程,然后分别启动两个线程,等待线程执行完之后输出x,y的值。同志们可以猜猜结果是多少。(注释后面的标号代表语句编号)

没错,根本没有“正确”答案。我这里有四种答案:

结果:x=0, y=1;执行顺序:1, 2, 3, 4

结果:x=1, y=0;执行顺序:3, 4, 1, 2

结果:x=1, y=1;执行顺序:1, 3, 2, 4

结果:x=0, y=0;执行顺序:2, 4, 1, 3

(前面三种执行结果你多执行几次都会出现,后面的理论是存在。但是我没有执行出来,单颗CPU更容易出现这样的结果)

这就是并发的本质,你的代码不会按照你写顺序执行。前三个很容解释,两个线程可能会被“交替”执行,让人困惑的是第四个结果,解释这个就必须用到“流水线,乱序执行、分支预测”。

CPU内部有多个执行单元(如果是多个CPU那就更多执行单元了),为了提高吞吐量,它会采用流水线同时执行多条指令;为了优化程序执行的效率适应流水线,CPU会分析指令的依赖关系把可以并行执行的指令并行执行。

在one线程中,a=1和y=b是没有任何依赖关系的,所以可能y=b会被先执行,a=1则后执行。同样的道理other线程中也是如此。

总结

没错,存储访问引起的不一致性+CPU为了提高效率引入的并行机制就是并发程序设计的困难,这两个问题结合在一起就是“Memory barrier”(内存屏障、内存栅栏),这不是Java独有的,在任何编程语言中都会存在这个问题,除非你的CPU不是多级存储、没有流水线(这还是CPU吗?)。

原文作者:写程序的康德     原文链接:https://mp.weixin.qq.com/s?__biz=MzIxMjAzMDA1MQ==&mid=2648945421&idx=1&sn=98b9c1b5fa004ec49d1f9f0484185c38#rd

相关文章
|
12天前
|
安全 Java Go
Java vs. Go:并发之争
【4月更文挑战第20天】
18 1
|
12天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
1天前
|
安全 Java
Java中的并发编程:理解并发性与线程安全
Java作为一种广泛应用的编程语言,在并发编程方面具有显著的优势和特点。本文将探讨Java中的并发编程概念,重点关注并发性与线程安全,并提供一些实用的技巧和建议,帮助开发人员更好地理解和应用Java中的并发机制。
|
1天前
|
搜索推荐 Java Shell
8大Java排序方法(由简入繁),有代码详解和原理指导
8大Java排序方法(由简入繁),有代码详解和原理指导
6 0
|
7天前
|
存储 安全 Java
【Java EE】CAS原理和实现以及JUC中常见的类的使用
【Java EE】CAS原理和实现以及JUC中常见的类的使用
|
9天前
|
监控 搜索推荐 算法
Java排序:原理、实现与应用
【4月更文挑战第28天】本文探讨了Java中的排序算法,包括原理和实现。Java利用Comparator接口进行元素比较,通过Arrays和Collections类的sort方法对数组和列表进行排序。示例展示了使用这些方法的基本代码。此外,还讨论了冒泡排序算法和自定义排序场景,以适应不同需求。理解这些排序机制有助于提升程序效率。
10 1
|
9天前
|
设计模式 消息中间件 Java
Java 设计模式:探索发布-订阅模式的原理与应用
【4月更文挑战第27天】发布-订阅模式是一种消息传递范式,被广泛用于构建松散耦合的系统。在 Java 中,这种模式允许多个对象监听和响应感兴趣的事件。
26 2
|
9天前
|
设计模式 算法 Java
Java 设计模式:深入模板方法模式的原理与应用
【4月更文挑战第27天】模板方法模式是一种行为设计模式,主要用于定义一个操作中的算法的框架,允许子类在不改变算法结构的情况下重定义算法的某些特定步骤。
19 1
|
12天前
|
安全 Java 开发者
Java编程:深入探索其原理、特性与实战代码
Java编程:深入探索其原理、特性与实战代码
12 1
|
13天前
|
Java API
[Java 并发基础]多线程编程
[Java 并发基础]多线程编程