最近在阅读《Java高并发程序设计》一书时,在 ConcurrentLinkedQueue 类源码分析部分,看到了这样一句代码:t != (t = tail)
。第一眼看见这句代码时,下意识的就将其理解为以下两个步骤:
错误理解:
1.赋值:t = tail;
2.比较 t != t。
相信不少人在初看这段代码时也会产生这样的反应,显然,如果是这种逻辑的话,那结果将永远是 false
,这种低级错误应该是不会发生在官方的源码中的。关于t != (t = tail)
,如果单从语义上理解的话,可以将其理解为以下两个步骤:
1.比较 t 与 tail 是否相等,即首先判断语句 t != tail;
2.将 tail 赋值给 t,即执行 t = tail;
为验证次说法,这里编写了一段简单的代码,如下:
public class EqualityJudgment {
public static void main(String[] args) {
String tail = "";
String t = "a";
boolean check = (t != (t = tail));
System.out.print("(t != (t = tail)) = " + check);
}
}
// 执行结果为:(t != (t = tail)) = true
显然,从执行结果来看,是符合上述第二种语义的,为进一步验证这种说法,可以利用 javap 工具查看t != (t = tail)
在字节码层面究竟是如何工作的。
打开命令行,进入上述代码编译之后生成的 class 文件所在的文件夹,输入 javap -v EqualityJudgment(javap 的使用方法可以输入 javap -help 查看,具体格式为 javap < options > < classes >…),这里截取了部分相关的字节码,如下所示:
就上图出现的部分相关的字节码,在下面的表格中给出了说明:
指令 | 含义 |
---|---|
ldc | 将int 、 float 或 String 型常量值从常量池中推送至栈顶 |
astore_n | 将栈顶引用型数值存入第 n 个本地变量(n = 1~4) |
aload_n | 将第 n 个引用类型本地变量推送至栈顶(n = 1~4) |
dup | 复制栈顶数值并将复制值压入栈顶 |
if_acmpeq | 比较栈顶两 int 型数值的大小,当结果等于 0 时跳转(此处的 0 代表 false) |
iconst_n | 将 int 型 n 推送至栈顶(n = -1~5,-1对应的指令为 iconst_m1) |
goto | 无条件跳转 |
将字节码指令与上表相对应,下图给出了简略的分析:
从上图可以看到,t != (t = tail)
在字节码层面的执行顺序大致是这样的:
1.将 t 的值压入栈顶;
2.将 tail 的值压入栈顶;
3.复制 tail 的值并将其压入栈顶;
4.将栈顶的值(即tail的值“a”)弹出并赋值给本地变量中的 t(此时本地变量中 t = “a”,但堆栈中的 t = “” 依旧是原值);
5.弹出并比较栈顶两元素,由于堆栈中的 t = “” 为原值,所以比较结果应该为0,即不相等;
6.故最后t != (t = tail)
的判断结果为 true。
可以看到,在字节码层面上,虽然赋值是先进行的,但由于此次赋值是给本地变量赋值,而堆栈中的元素 t 是在赋值操作前被压入的,其值依旧为原值,所以此处的比较其实是 t 的原值在和 tail 作比较,结果当然就是不相等啦。当然,从代码表面的语义上看,t != (t = tail)
就像是做了这么一件事:首先比较 t != tail,然后赋值 t = tail。