由一个简单的例子引出并发处理时容易被忽视的陷阱,用来作为面试问题应该很适合。
某日,工作了 4 年多的 Java 程序员小 K 跳槽,面试时碰到这样一个题目....
public class P1 {
private long b = 0 ;
public void set1() {
b = 0 ;
}
public void set2() {
b = - 1 ;
}
public void check() {
System.out.println(b);
if ( 0 != b && - 1 != b) {
System.err.println( "Error" );
}
}
} |
问题
调用 set1()、set2()、check(),会打印出 Error 么?
小K 的推理
“无论如何调用 set1()、set2() -> b 的值只可能是 0 或 -1 -> 在 check() 里面的判断条件(b 既不为 0 也不为 -1)永远不成立 -> 不打印 Error”
小 K 觉得有坑:这题目应该不会这么简单,再考虑一下多线程环境。
思前想后,小 K 得出结论:“在多线程环境下也不会打印 Error。这题目很简单,就是考察一下推理吧。”,K 暗自窃喜。
后来小 K 陆续又被问了几个多线程和 JVM 的问题。
后来,就没有后来了....
后来
后来还是有的。到家后,不甘心的小 K 验证了这道秒杀他的面试题。
public static void main( final String[] args) {
final P1 v = new P1();
// 线程 1:设置 b = 0
final Thread t1 = new Thread() {
public void run() {
while ( true ) {
v.set1();
}
};
};
t1.start();
// 线程 2:设置 b = -1
final Thread t2 = new Thread() {
public void run() {
while ( true ) {
v.set2();
}
};
};
t2.start();
// 线程 3:检查 0 != b && -1 != b
final Thread t3 = new Thread() {
public void run() {
while ( true ) {
v.check();
}
};
};
t3.start();
} |
使用 3 个线程分别重复执行 set1()、set2()、check()。执行输出结果部分如下:
.... 0 0 1 1 1 Error Error -4294967296 0 0 4294967295 .... |
执行环境:
Java(TM) SE Runtime Environment (build 1.6.0_31-b05)
Java HotSpot(TM) Client VM (build 20.6-b01, mixed mode, sharing), 32bit
“确实打印了 Error,并且打印了 4294967295、-4294967296。我勒个去,只是啥情况?”
小 K 决定搞懂其中奥秘,重新审视了题目。以一个专业程序员的严谨,并经过无数次 Google 后....他似乎发现了问题所在。
“这确实是一个并发问题!”
分析
这道题目有两个陷阱,分别考察了对并发执行的理解,以及对 JVM 基础(赋值操作)的掌握。
陷阱一:并发执行
并发执行就是多个操作一起执行,CPU 执行不同上下文(可理解为不同线程)发过来的指令。操作系统上层看上去就像是并行处理一样。
也就是说,在编程语言层面,一个简单的操作同样需要考虑并发问题。
小 K 首先是栽在了 check() 中的 if 判断上和设值是存在并发的,不能保证 0 != b 这个判断真(此时 b 为 -1)后恰好 b 被赋值为 0 时判断 1 != b。
除此外,无论 JVM、操作系统、CPU 层面对指令如何优化、重排,最终都是逐一执行单一指令,唯一不同的就是不同层面可能会对执行加以限制,
比如加入原子操作,最终保证 CPU 能够完整执行一组指令。
陷阱二:JVM 赋值操作
一些赋值操作不是原子性的。“纳尼?”
Java 基础类型中,long 和 double 是 64 位长的。32 位架构 CPU 的算术逻辑单元(ALU)宽度是 32 位的,在处理大于 32 位操作时需要处理两次。
“这不是<计算机组成原理与汇编>么”,小 K 顿时感到大学白上了,不懂学以致用 T_T~
题目执行打印 4294967295、-4294967296 就是因为读时高 32 位或低 32 位被其他写覆盖了(看一下这两个数字的二进制就知道了)。
Java 已经是封装底层细节很好的语言了,但依然需要注意这些陷阱,可以使用并发处理包 java.util.concurrent.atomic 中包含了一系列无锁原子操作类型,
也可以使用 volatile 关键字保证线程间变量的可见性。
其实这道题目只要解决了并发问题,也就保证了每个执行单元(set1()、set2()、check())中赋值、比较的正确性。可以把同步方法执行看作序列化的事务,各中操作不会相互影响。
再后来
虽然小 K 面试挂了,不过他挂得心服口服。
通过这个期间的不断翻阅文档以及实验,小 K 下次的面试应该不会被类似的题目秒杀了吧....
“按照这个简单面试题的标准,以前写过的程序简直就是通篇 bugs 啊!有木有,有木有啊!!!!”
非原子的64位操作
当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性( out-of-thin-air safety)。
最低安全性适用于绝大多数变量,但是存在一个例外:非volatile 类型的64位数值变量(double和long,请参见3.1.4节)。Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来。
相关推荐
Go提供了我们便利的进行并发编程的工具、方法和同步原语,同时也提供给我们诸多的犯错的机会,也就是俗称的“坑”。即使是顶级Go开发的项目,比如Docker、Kubernetes、gRPC、etcd, 都是有经验丰富的Go开发专家锁...
java并发编程实践这本教程深入讲解了Java并发编程的相关知识点。首先,它详细探讨了Java内存模型。Java内存模型定义了共享变量的访问规则,以及线程间的通信方式。在JVM中,每个线程有自己的工作内存,共享变量从主...
书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及验证线程安全的规则,如何将小的线程安全类组合成更大的线程安全类,如何利用线程来提高...
并发易学难精,没有稳扎稳打的第一步,前路坎坷易弃坑。本课程涵盖线程、进程、多线程、并发、高并发、同步、异步、阻塞、非阻塞等,带大家快速构建清晰的理论基石。另有高频面试点拨,让你学懂、会用。
在Golang编程实践中,有一些常见的坑和编程模式是开发者需要特别注意的。首先,Golang是一门以其简洁、高效、并发特性著称的编程语言,它提供了良好的并发支持、静态链接、简洁直观的语法以及语言级别的并发与自动化...
Go提供了我们便利的进行并发编程的工具、方法和同步原语,同时也提供给我们诸多的犯错的机会,也就是俗称的“坑”。即使是顶级Go开发的项目,比如Docker、Kubernetes、gRPC、etcd, 都是有经验丰富的Go开发专家所...
9. **项目代码问题的反思**:通过分析项目中的问题,开发者可以学习如何避免常见错误,如内存泄漏、并发问题等,提升代码质量。 10. **代码的可扩展性**:为了未来的修改和增强,代码应该设计得易于扩展。例如,...
本书重点介绍Go语言的实践和并发编程范式,力求让读者不但清楚并发的基本语法和原理,还学会怎么去使用。本书对Go语言规范中的命名类型、非命名类型,底层类型,动态类型等概念进行阐述,让*发者对Go的类型系统有...
Go语言的并发模型基于CSP(Communicating Sequential Processes)理论,提供了goroutine和channel等并发原语来实现高性能的并发编程。此外,Go还支持传统的多线程并发机制,以及并发控制中的常见模式,比如互斥锁、...
【描述】:本章节探讨的是如何利用Java 8引入的新特性`CompletableFuture`进行高效的并发编程,尤其是在处理复杂的异步计算场景时,它提供了一种强大的工具。 【标签】:“java”、“并发”、“编程”、“宝典” ...
这份“Java避坑指南:Java高手笔记代码篇”旨在帮助开发者绕过这些潜在的障碍,提升编程效率和代码质量。以下是一些关键的知识点,从标题和描述中可以提炼出来: 1. **异常处理**:Java中的异常处理是程序员必须...
跟无闻学Go语言:Go编程基础视频教程 的ppt讲义 第 1 课:Go 开发环境搭建 第 2 课:Go基础知识 第 3 课:类型与变量 第 4 课:常量与运算符 第 5 课:控制语句 第 6 课:数组 array 第 7 课:切片 slice 第 8 课:...
Java并发编程的艺术 , Java TCP_IP Socket编程(2版), 深入理解Java虚拟机(第2版), 机器学习实践指南:案例应用解析(第2版) 非常建议平时多读读前两本书,虽然它们是最短的,确实最有用的,阿里的建议还是很...
本资源摘要信息涵盖了 Java 后端开发的多个方面,包括 JPA 防踩坑姿势、Servlet 3 异步原理与实践、Tomcat 源码剖析、Java 并发编程、Java 线程池、AbstractQueuedSynchronizer、Tomcat 系列、Netty 系列、Kafka ...
在编程世界中,Go语言,又称为Golang,是由Google开发的一种静态类型的、编译型的、并发型的、垃圾回收的、具有C风格语法的编程语言。它以其简洁的语法、高效的性能以及内置的并发支持赢得了广大开发者喜爱。然而,...
此外,Scala支持函数式编程,这使得它在处理并发和分布式系统时表现出色,因为函数式编程避免了共享状态,减少了并发问题。Scala的语法简洁,能够创建优雅的API,这极大地提高了代码的可读性和维护性。由于其与Java...
Go语言以其goroutines和channel为并发编程提供了强大的工具。然而,不恰当的goroutine管理和channel使用可能导致死锁、数据竞争或资源浪费。正确地使用select、sync包中的Mutex和WaitGroup是避免这些问题的关键。 ...
2. Go语言是一门由Google开发的静态类型、编译型语言,它结合了操作系统的并发能力和高级语言的编程便捷性,非常适合用于系统编程领域。 3. “我在GO里趟过的坑”很可能是指叶其春在使用Go语言进行开发时遇到的一些...