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

相关文章
|
3月前
|
监控 Java API
现代 Java IO 高性能实践从原理到落地的高效实现路径与实战指南
本文深入解析现代Java高性能IO实践,涵盖异步非阻塞IO、操作系统优化、大文件处理、响应式网络编程与数据库访问,结合Netty、Reactor等技术落地高并发应用,助力构建高效可扩展的IO系统。
115 0
|
4月前
|
存储 缓存 Java
我们来详细讲一讲 Java NIO 底层原理
我是小假 期待与你的下一次相遇 ~
169 2
|
3月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
321 83
|
3月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
300 83
|
4月前
|
存储 算法 安全
Java中的对称加密算法的原理与实现
本文详细解析了Java中三种常用对称加密算法(AES、DES、3DES)的实现原理及应用。对称加密使用相同密钥进行加解密,适合数据安全传输与存储。AES作为现代标准,支持128/192/256位密钥,安全性高;DES采用56位密钥,现已不够安全;3DES通过三重加密增强安全性,但性能较低。文章提供了各算法的具体Java代码示例,便于快速上手实现加密解密操作,帮助用户根据需求选择合适的加密方案保护数据安全。
373 58
|
3月前
|
人工智能 安全 Java
Go与Java泛型原理简介
本文介绍了Go与Java泛型的实现原理。Go通过单态化为不同类型生成函数副本,提升运行效率;而Java则采用类型擦除,将泛型转为Object类型处理,保持兼容性但牺牲部分类型安全。两种机制各有优劣,适用于不同场景。
107 24
|
4月前
|
Java 物联网 数据处理
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
Java Solon v3.2.0 是一款性能卓越的后端开发框架,新版本并发性能提升700%,内存占用节省50%。本文将从核心特性(如事件驱动模型与内存优化)、技术方案示例(Web应用搭建与数据库集成)到实际应用案例(电商平台与物联网平台)全面解析其优势与使用方法。通过简单代码示例和真实场景展示,帮助开发者快速掌握并应用于项目中,大幅提升系统性能与资源利用率。
142 6
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
|
4月前
|
XML JSON Java
Java 反射:从原理到实战的全面解析与应用指南
本文深度解析Java反射机制,从原理到实战应用全覆盖。首先讲解反射的概念与核心原理,包括类加载过程和`Class`对象的作用;接着详细分析反射的核心API用法,如`Class`、`Constructor`、`Method`和`Field`的操作方法;最后通过动态代理和注解驱动配置解析等实战场景,帮助读者掌握反射技术的实际应用。内容翔实,适合希望深入理解Java反射机制的开发者。
396 13