Java并发原理无废话指南

本文剖析了并发编程中的两大核心概念:多级存储和CPU指令乱序执行。通过具体实例展示了这些概念如何影响程序的行为,并解释了如何利用volatile关键字解决多线程间的可见性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


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

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

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

  1. CPU访问存储的方式——多级存储;
  2. CPU执行指令的方式——乱序

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

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

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

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

多级存储

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

代码示例

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

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

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

这里写图片描述

操作系统把我们程序中的主进程和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效率低,无法保证原子性所以不安全)。

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

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

这里写图片描述

我定义了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吗?)。


欢迎关注公众账号了解更多信息

欢迎关注公众账号了解更多信息

线程的状态以及各状态之间的转换详解.mp4 线程的初始化,中断以及其源码讲解.mp4 多种创建线程的方式案例演示(一)带返回值的方式.mp4 多种创建线程的方式案例演示(二)使用线程池.mp4 Spring对并发的支持:Spring的异步任务.mp4 使用jdk8提供的lambda进行并行计算.mp4 了解多线程所带来的安全风险.mp4 从线程的优先级看饥饿问题.mp4 从Java字节码的角度看线程安全性问题.mp4 synchronized保证线程安全的原理(理论层面).mp4 synchronized保证线程安全的原理(jvm层面).mp4 单例问题与线程安全性深入解析.mp4 理解自旋锁,死锁与重入锁.mp4 深入理解volatile原理与使用.mp4 JDK5提供的原子类的操作以及实现原理.mp4 Lock接口认识与使用.mp4 手动实现一个可重入锁.mp4 AbstractQueuedSynchronizer(AQS)详解.mp4 使用AQS重写自己的锁.mp4 重入锁原理与演示.mp4 读写锁认识与原理.mp4 细读ReentrantReadWriteLock源码.mp4 ReentrantReadWriteLock锁降级详解.mp4 线程安全性问题简单总结.mp4 线程之间的通信之wait notify.mp4 通过生产者消费者模型理解等待唤醒机制.mp4 Condition的使用及原理解析.mp4 使用Condition重写waitnotify案例并实现一个有界队列.mp4 深入解析Condition源码.mp4 实战:简易数据连接池.mp4 线程之间通信之join应用与实现原理剖析.mp4 ThreadLocal 使用及实现原理.mp4 并发工具类CountDownLatch详解.mp4 并发工具类CyclicBarrier 详解.mp4 并发工具类Semaphore详解.mp4 并发工具类Exchanger详解.mp4 CountDownLatch,CyclicBarrier,Semaphore源码解析.mp4 提前完成任务之FutureTask使用.mp4 Future设计模式实现(实现类似于JDK提供的Future).mp4 Future源码解读.mp4 ForkJoin框架详解.mp4 同步容器与并发容器.mp4 并发容器CopyOnWriteArrayList原理与使用.mp4 并发容器ConcurrentLinkedQueue原理与使用.mp4 Java中的阻塞队列原理与使用.mp4 实战:简单实现消息队列.mp4 并发容器ConcurrentHashMap原理与使用.mp4 线程池的原理与使用.mp4 Executor框架详解.mp4 实战:简易web服务器(一).mp4 实战:简易web服务器(二).mp4 JDK8的新增原子操作类LongAddr原理与使用.mp4 JDK8新增锁StampedLock详解.mp4 重排序问题.mp4 happens-before简单概述.mp4 锁的内存语义.mp4 volatile内存语义.mp4 final域的内存语义.mp4 实战:问题定位.mp4
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值