网站首页 > 精选教程 正文
前文介绍了Java内存模型(JMM: Java Memory Model)的设计始终围绕着多线程并发的三个重要特性:原子性(Atomicity)、可见性(Visibility)和有序性(Ordering)。在并发编程中,我们要始终注意如何处理这三个特性的问题。
本文重点讨论如何保证多线程的有序性。一句话说明有序性规则:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
有序性规则主要体现在两个场景:
- 线程内:从单个线程来看,指令会按照类似“串行”(as-if-serial)的方式顺序执行;
- 线程外:这个线程“观察”其他线程并发地执行非同步代码时,由于指令重排序优化,代码有可能交叉执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序(Reordering),但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
指令重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。指令重排序分为三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP: Instruction-Level Parallelism)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
先行发生原则
Java内存模型本身也有一些先天有序性的约束,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 先行发生原则。
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的 start() 方法先行发生于此线程的每一个动作
- 线程中断规则:对线程 interrupt() 中断方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测
- 对象终结规则:一个对象的初始化完成先行发生于它的 finalize() 方法的开始
先行发生原则是我们用来判断数据是否存在竞争、线程是否安全的主要依据。如果操作A先行发生于操作B,那么操作A产生的影响一定能够被操作B观察到。
需要注意的是,由于重排序的因素,一个操作先行发生并不代表这个操作在时间上是先发生的。
看一个例子
有这样两个线程A和B:线程A为变量 a 赋值,赋值后设置标志 isAssigned;线程B通过 isAssigned 判断是否已完成赋值,如果完成那么就利用变量 a 来计算。
在线程A中,由于两个赋值语句之间没有数据依赖性,因此处理器可能会对指令重排序,调换两个指令的顺序,在单线程内不会对结果造成影响,是不违背happens-before规则的。但是可能会破坏多线程语义。
如果两个线程以上图的顺序运行,那么就可能会出现:线程B在变量被赋值前,就先使用变量进行计算。
保证有序性的方法
在 Java 中,可以使用 synchronized 和 volatile 来保证多线程之间操作的有序性。
(1)volatile 关键字会禁止指令重排序。对于上面的例子,可以将 isAssigned 定义为 volatile 变量。
volatile boolean isAssigned = false;
这样线程A中 对变量a的赋值语句,就不会排序到 isAssigned = true 之后。
(2)synchronized 关键字通过互斥保证同一时刻只允许一条线程操作。
我们可以把线程A和线程B中的相关方法或代码块,定义为同步方法或同步代码块。
由于线程A的代码是同步方法,在线程A获取锁以后,执行过程中不会被线程B打断,这样就保证了 a=1 和 isAssigned=true 这两个指令会一起完成。不会出现线程B判断赋值已完成,但实际上 a 尚未完成赋值的情况出现。
总结
为了充分利用现代处理器的能力,编译器和处理器都可以对指令重排序,但有可能会破坏多线程运行的语义。为了保证程序执行的正确性,在JMM的先行发生原则基础上,使用 synchronized 和 volatile来确保多线程之间操作的有序性。
我会持续更新关于物联网、云原生以及数字科技方面的文章,用简单的语言描述复杂的技术,也会偶尔发表一下对IT产业的看法,欢迎大家关注,谢谢。
- 上一篇: Java获取多线程执行结果方式的归纳与总结
- 下一篇: JAVA多线程实现的四种方式
猜你喜欢
- 2025-01-21 Java多线程知识点详解,包含实例,搞懂多线程看这一篇文章就够了
- 2025-01-21 字节Java全能手册火了!多线程/网络/性能调优/框架啥都有
- 2025-01-21 Java多线程中——部分场景使用实现
- 2025-01-21 探究Java多线程(下篇):理论+工具+性能调优+电商场景下的使用
- 2025-01-21 JAVA多线程实现的四种方式
- 2025-01-21 Java获取多线程执行结果方式的归纳与总结
- 2025-01-21 什么!还不懂Java多线程编程?快来看看阿里资深架构师的熬夜总结
- 2025-01-21 项目案例:Java多线程批量拆分List导入数据库
- 2025-01-21 Redis不是号称单线程效率也很高吗,为什么又采用多线程了?
- 2025-01-21 Java 多线程神器:CountDownLatch 让线程排队,不再磨蹭
你 发表评论:
欢迎- 07-10动漫人物像|插画 壁纸 头像 签名 素材
- 07-10运动人物|插画 壁纸 头像 签名 素材
- 07-10动漫人物|插画 壁纸 头像 签名 素材
- 07-10神话人物|插画 壁纸 头像 签名 素材
- 07-10日漫人物像|插画 壁纸 头像 签名 素材
- 07-10 日漫人物|插画 壁纸 头像 签名 素材
- 07-10日漫人物风|插画 壁纸 头像 签名 素材
- 07-10日漫人物|插画 壁纸 头像 签名 素材
- 最近发表
- 标签列表
-
- nginx反向代理 (57)
- nginx日志 (56)
- nginx限制ip访问 (62)
- mac安装nginx (55)
- java和mysql (59)
- java中final (62)
- win10安装java (72)
- java启动参数 (64)
- java链表反转 (64)
- 字符串反转java (72)
- java逻辑运算符 (59)
- java 请求url (65)
- java信号量 (57)
- java定义枚举 (59)
- java字符串压缩 (56)
- java中的反射 (59)
- java 三维数组 (55)
- java插入排序 (68)
- java线程的状态 (62)
- java异步调用 (55)
- java中的异常处理 (62)
- java锁机制 (54)
- java静态内部类 (55)
- java怎么添加图片 (60)
- java 权限框架 (55)
本文暂时没有评论,来添加一个吧(●'◡'●)