Java面试——原子操作与并发

一道“简单”的面试题, 直接上代码

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 么?

求真
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  
执行环境:
    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。我勒个去,神马情况?”
重新审视题目,以一个专业程序员的严谨,并经过无数次 Google 后....似乎发现了问题所在。

“这确实是一个并发问题!”

分析

这道题目有两个陷阱,分别考察了对并发执行的理解,以及对 JVM 基础(赋值操作)的掌握。

陷阱一:并发执行

并发执行就是多个操作一起执行,CPU 执行不同上下文(可理解为不同线程)发过来的指令。操作系统上层看上去就像是并行处理一样。
也就是说,在编程语言层面,一个简单的操作同样需要考虑并发问题。

check() 中的 if 判断上和设值是存在并发的,不能保证 0 != b 这个判断真(此时 b 为 -1)后恰好 b 被赋值为 0 时判断 1 != b。

除此外,无论 JVM、操作系统、CPU 层面对指令如何优化、重排,最终都是逐一执行单一指令,唯一不同的就是不同层面可能会对执行加以限制,比如加入原子操作,最终保证 CPU 能够完整执行一组指令。

陷阱二:JVM 赋值操作

一些赋值操作不是原子性的。“纳尼?”

Java 基础类型中,long 和 double 是 64 位长的。32 位架构 CPU 的算术逻辑单元(ALU)宽度是 32 位的,在处理大于 32 位操作时需要处理两次。

“这不是<计算机组成原理与汇编>么”,顿时感到大学白上了,不懂学以致用。。。

题目执行打印 4294967295、-4294967296 就是因为读高 32 位或低 32 位时被其他写覆盖了(看一下这两个数字的二进制就知道了)。

Java 已经是封装底层细节很好的语言了,但依然需要注意这些陷阱,可以使用并发处理包 java.util.concurrent.atomic 中包含了一系列无锁原子操作类型,也可以使用 volatile 关键字保证线程间变量的可见性。

其实这道题目只要解决了并发问题,也就保证了每个执行单元(set1()、set2()、check())中赋值、比较的正确性。可以把同步方法执行看作序列化的事务,各中操作不会相互影响。

继续充电吧!