一些新的记录会写在issue里面
seaswalker / jdk-sourcecode-analysis Goto Github PK
View Code? Open in Web Editor NEWJDK源码阅读笔记
JDK源码阅读笔记
一些新的记录会写在issue里面
Sent with GitHawk
根据: The JSR-133 Cookbook for Compiler Writers,Java内存模型定义了四种内存屏障:
其实就是load和store操作的全排列。
而Linux定义了三种内存屏障:
smp_rmb
smp_wmb
smp_wb
从字面上来看,smp_rmb
对应LoadLoad,smp_wmb
对应StoreStore,smp_mb
对应LoadStore和StoreLoad,这样来说Java的定义更加细致一些,区分了Load和Store重排的两个不同方向。
引用<Is Parallel Programming Hard, And, If So, What Can You Do About It?>中对它作用的说明:
The effect of this is that a read memory barrier orders only loads on the CPU that executes it, so that all loads preceding the read memory barrier will appear to have completed before any load following the read memory barrier.
那么什么会导致相反地结果?我觉得可能有两种情况:
那么在这种情况下读屏障可以来保证顺序,怎么做到呢,猜测是禁止重排并且在第一条完成加载后再执行第二条。
Is Parallel Programming Hard, And, If So, What Can You Do About It?书中给出了在存在Invalidate Queue的情况下读屏障的作用,代码如下:
// CPU 0执行,假设a处于share状态,b被CPU 0独占
void foo(void) {
a = 1;
smp_wmb();
b = 1;
}
void bar(void) {
while (b == 1) continue;
assert (a == 1);
}
即使我们读到了b == 1
,a还是有可能为0并导致断言失败。这里的原因并不是重排,而是执行bar
函数的CPU没有将Invalidate Queue中的使无效消息应用到cache中,所以导致CPU读到了无效(过期)的a值。
所以需要一个读屏障来drain Invalidate Queue:
void bar(void) {
while (b == 1) continue;
smp_rmb();
assert (a == 1);
}
所以我认为这里读屏障有两种实际的作用:禁止重排和drain Invalidate Queue。
这两点就保证了Java的LoadLoad和Linux的smp_rmb
只是针对编译器的,而在硬件层面是一个nop
。
通常CPU有store buffer这一结构(其实我也不知道是不是全都有,但X86和ARM是都存在)。假设CPU先后执行两个store操作:
a = 1;
b = 2;
和Load类似,我认为也有两种情形会导致b先于a被存储:
所以写屏障的作用就是:
x86平台会把所有写操作放到store buffer中,并保证写操作的先后顺序,所以在x86上StoreStore也是只针对编译器,对硬件是一个nop
。
x86保证了前面的load会先于store发生,至于原理不得而知,可能和store是异步的,而load是实时的有关。所以在x86上Java的LoadStore是一个只针对编译器的nop
。
这是在x86上唯一没有被保证的,原因猜测是store是异步操作,load很容易跑到store前面。Java使用lock
指令前缀来实现,而mfence
也有同样的效果。
这一篇文章实锤了x86不保证StoreLoad: Memory Reordering Caught in the Act
mfence
的作用是排干store buffer,在这之前封锁后续load操作。
但是是否x86保证了顺序,就意味着实际上完全没有重排发生了呢?可能不是这样的,重排可能还是有的,只不过是不可见的,比如对a和b的写入,两个变量恰好位于当前CPU的同一个缓存行,处于独占状态,其它CPU无法观察到两个变量写入的先后顺序。
参考: Does an x86 CPU reorder instructions?
感觉内存屏障的细节真的很难理解,因为需要知道CPU内部的实现细节,而这一点则很难实现,比如对于ARM处理器就无法知道其store buffer是不是FIFO,有没有Invalidate Queue。
以NonfairSync为例:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
关键在于compareAndSetState方法:
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
* This operation has memory semantics of a {@code volatile} read
* and write.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that the actual
* value was not equal to the expected value.
*/
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
注释里写了,此方法有相当于volatile读写的内存语义。所以这个内存语义又是什么?
参考Doug lea的: The JSR-133 Cookbook for Compiler Writers
- Issue a StoreStore barrier before each volatile store
- Issue a StoreLoad barrier after each volatile store.(Alternatively, if available, you can implement volatile store as an atomic instruction (for example XCHG on x86) and omit the barrier. This may be more efficient if atomic instructions are cheaper than StoreLoad barriers.)
- Issue LoadLoad and LoadStore barriers after each volatile load.
unsafe.compareAndSwapInt由Atomic::cmpxchg实现(Linux):
inline jlong Atomic::cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value) {
bool mp = os::is_MP();
__asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
所以这在x86上就是加了lock前缀的cmpxhgg指令,而lock前缀在intel上便充当了读写memory barrier的作用,来自书中的摘抄L:
On the other hand, x86 CPUs have traditionally given no ordering guarantees for loads, so the smp_mb() and smp_rmb() primitives expand to lock;addl. This atomic instruction acts as a barrier to both loads and stores.
只看一行:
protected final boolean tryRelease(int releases) {
// ...
setState(c);
// ...
}
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
从前面Doug lea的文章中可以看出,volatile
写会导致在后面追加一个StoreLoad屏障,而此屏障在x86上:
So,也许又是一条lock前缀指令。
读了一次“错误”的并发控制引发的思考一文,觉得有些疑问。对于下面的代码:
class MultiProcessorTask {
private boolean flag = true;
public void runMethod() {
while (flag) {
synchronized (new Simple(1)){}
}
}
public void stopMethod() {
System.out.println("change 'flag' field ...");
flag = false;
}
}
原文的观点似乎倾向于synchronized
带来的happens-before规则可以保证对flag
的可见性,所以需要用JVM参数-XX:-EliminateLocks
关闭锁消除优化就行了。
我的疑问在于:
所以我把代码改写成了没有锁和volatile
:
package test;
class MultiProcessorTask {
private boolean flag = true;
long sum = 0L;
public void runMethod() {
while (flag) {
long a = System.currentTimeMillis() % 9;
if (a == 1L) {
sum += a;
}
}
System.out.println("Result: " + sum);
}
public void stopMethod() throws InterruptedException {
System.out.println("准备睡眠1秒,然后置flag为false.");
Thread.sleep(1000);
System.out.println("change 'flag' field ...");
flag = false;
}
}
class ThreadA extends Thread {
private MultiProcessorTask task;
ThreadA(MultiProcessorTask task) {this.task = task;}
@Override
public void run() {
task.runMethod();
}
}
public class TestRun {
public static void main(String[] args) throws InterruptedException {
MultiProcessorTask task = new MultiProcessorTask();
ThreadA a = new ThreadA(task);
a.start();
task.stopMethod();
System.out.println("it's over");
}
}
直接运行,不会退出,加上JVM参数-Xint
解释执行,会退出,这一步就说明了这个锅还是JIT的,下面通过jitwatch看一下JIT优化后的汇编代码。使用的JVM参数是:
-server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=live.log
没有volatile
时:
L0001: movabs $0x108289ce4,%r10
0x00000001117cc1f6: callq *%r10 ;*invokestatic currentTimeMillis
; - test.MultiProcessorTask::runMethod@7 (line 10)
0x00000001117cc1f9: mov %rax,%r10
0x00000001117cc1fc: mov %rax,%r11
0x00000001117cc1ff: sar $0x3f,%r11
0x00000001117cc203: movabs $0x1c71c71c71c71c72,%rax
0x00000001117cc20d: mov %r10,%r8
0x00000001117cc210: imul %r10
0x00000001117cc213: sub %r11,%rdx ;*lrem
; - test.MultiProcessorTask::runMethod@13 (line 10)
0x00000001117cc216: mov %rdx,%r10
0x00000001117cc219: shl $0x3,%r10
0x00000001117cc21d: add %rdx,%r10
0x00000001117cc220: mov %r8,%r11
0x00000001117cc223: sub %r10,%r11
0x00000001117cc226: cmp $0x1,%r11
0x00000001117cc22a: jne L0002 ;*ifne ;如果余数不是1
; - test.MultiProcessorTask::runMethod@18 (line 11)
0x00000001117cc22c: incq 0x10(%rbp) ; OopMap{rbp=Oop off=144}
;*goto
; - test.MultiProcessorTask::runMethod@31 (line 14)
L0002: test %eax,-0xa279236(%rip) # 0x0000000107553000
;*goto
; - test.MultiProcessorTask::runMethod@31 (line 14)
; {poll} *** SAFEPOINT POLL ***
0x00000001117cc236: jmp L0001
L0003: xor %ebp,%ebp
0x00000001117cc23a: jmp L0000
可以看出,里面形成了一个死循环,不再判断flag
的值,甚至也不把为1的余数加到sum
中,每次循环只是取当前时间,然后取余。
而给flag
加上volatile
后的汇编代码为:
0x0000000119be7861: jmp L0002
L0000: mov 0x10(%rbx),%r10 ;*getfield sum; 余数是1时跳到这里,取sum加总
; - test.MultiProcessorTask::runMethod@23 (line 12)
0x0000000119be7867: add $0x1,%r10
0x0000000119be786b: mov %r10,0x10(%rbx) ;*putfield sum
; - test.MultiProcessorTask::runMethod@28 (line 12)
0x0000000119be786f: nop ; OopMap{rbx=Oop off=80}
;*goto
; - test.MultiProcessorTask::runMethod@31 (line 14)
L0001: test %eax,-0xc6fd876(%rip) # 0x000000010d4ea000; 余数不是1跳到这里,取flag测试继续循环
;*aload_0
; - test.MultiProcessorTask::runMethod@0 (line 9)
; {poll} *** SAFEPOINT POLL ***
L0002: movzbl 0xc(%rbx),%r11d ;*getfield flag
; - test.MultiProcessorTask::runMethod@1 (line 9)
0x0000000119be787b: test %r11d,%r11d; 测试flag是不是为false
0x0000000119be787e: je L0003 ;*ifeq; 是false,跳到L0003退出循环
; - test.MultiProcessorTask::runMethod@4 (line 9)
0x0000000119be7880: movabs $0x10e289ce4,%r10
0x0000000119be788a: callq *%r10 ;*invokestatic currentTimeMillis
; - test.MultiProcessorTask::runMethod@7 (line 10)
0x0000000119be788d: mov %rax,%r11
0x0000000119be7890: movabs $0x1c71c71c71c71c72,%rax
0x0000000119be789a: imul %r11
0x0000000119be789d: mov %r11,%r10
0x0000000119be78a0: sar $0x3f,%r10
0x0000000119be78a4: sub %r10,%rdx ;*lrem
; - test.MultiProcessorTask::runMethod@13 (line 10)
0x0000000119be78a7: mov %rdx,%r10
0x0000000119be78aa: shl $0x3,%r10
0x0000000119be78ae: add %rdx,%r10
0x0000000119be78b1: sub %r10,%r11
0x0000000119be78b4: cmp $0x1,%r11
0x0000000119be78b8: je L0000 ;*ifne; 如果余数是1,那么跳到L0000
; - test.MultiProcessorTask::runMethod@18 (line 11)
0x0000000119be78ba: jmp L0001; 余数不是1,跳到L0001
L0003: mov $0xffffff65,%esi
代码不同一目了然了。所以,在针对单个变量的前提下,不管是volatile
还是加锁各种花式操作,所针对的都不是硬件层面上的可见性问题,而是如何阻止JIT激进优化的问题。
两次汇编代码的优化级别都是:
其实,我的例子在不加volatile
的情况下使用JVM参数-XX:-UseOnStackReplacement
也能正常退出。
推荐一下:https://github.com/raysonfang/jdk1.8-source-analysis,从环境搭建到详细分析源码过程
不更新了吗
通常情况下关于volatile用法的正确例子是这样:
private volatile boolean flag = true;
@org.junit.Test
public void testVolatile() {
new Thread(() -> {
try {
System.out.println("子线程启动");
TimeUnit.SECONDS.sleep(3);
flag = false;
System.out.println("flag false");
} catch (InterruptedException ignore) {
}
}).start();
while (flag) {
}
}
结果也确实如此,不会导致死循环,与之相反如果把volatile去掉:
private boolean flag = true;
的确会死循环,于是我们得出结论:必须使用volatile
保证内存可见性。然而事实并不是如此,下面的例子只在while (flag)
中加入一句无关痛痒的代码:
private boolean flag = true;
@org.junit.Test
public void testVolatile() {
new Thread(() -> {
try {
System.out.println("子线程启动");
TimeUnit.SECONDS.sleep(3);
flag = false;
System.out.println("flag false");
} catch (InterruptedException ignore) {
}
}).start();
while (flag) {
System.out.print(1);
}
}
你会发现,现在也不会死循环了。
其实,在X86平台上,即使不使用memory barrier处理器也会保证变量修改的全局可见。参考:
Does a memory barrier ensure that the cache coherence has been completed?
上面那个例子死循环的原因其实是JVM编译器优化,将代码实际上变成了(加上volatile
或打印阻止了这种优化):
if (flag) {
while (true) {}
}
参考:请问R大 有没有什么工具可以查看正在运行的类的c/汇编代码
那X86既然已经保证了全局可见,为什么我们还要使用volatile
?Java毕竟是跨平台的语言,也许就会运行在不保证全局可见的处理器上,那时就会出错了。
所以如果你没在这种场景下用volatile
,并不是你写的对,而是你恰好把代码跑在了X86上而已。
这一段看的简直弯弯绕~
来自: Is Parallel Programming Hard, And, If So, What Can You Do About It?
即所有CPU(不同架构)均遵从的准则:
第二条有意思,暂时不太准确理解其意思。几句耐人寻味的原文摘抄过来:
Because current commercially available computer systems provide cache coherence, if a group of CPUs all do concurrent non-atomic stores to a single variable, the series of values seen by all CPUs will be consistent with at least one global ordering.
Please note well that this section applies only when all CPUs’ accesses are to one single variable. In this single-variable case, cache coherence guarantees the global ordering, at least assuming that some of the more aggressive compiler optimizations are disabled via the Linux kernel’s ACCESS_ONCE() directive or C++11’s relaxed atomics [Bec11]. In contrast, if there are multiple variables, memory barriers are required for the CPUs to consistently agree on the order for current commercially available computer systems.
最后一句话,如果是对多个变量进行操作,那么需要memory barrier,换句话说,如果只有一个变量,无需memory barrier.
锁的加锁和解锁操作必定以显性或隐性的方式存在memory barrier,这提供了内存顺序保证:第一个线程在临界区内的操作必定发生在第二个线程获得锁之前。Java的ReentrantLock的实现参考 #9
- 不能保证在内存屏障之前的内存访问将在内存屏障指令完成时完成;屏障仅仅用来在 CPU的访问队列中做一个标记,表示相应的访问不能被穿越。
- 不能保证在一个CPU中执行一个内存屏障将直接影响另外一个 CPU或者影响系统中其他硬件。间接效果是第二个CPU看到第一个CPU的访问是按顺序的。
- 不能保证一个CPU将看到第二个CPU 访问操作的正确顺序,即使第二个CPU使用一个内存屏障,除非第一个CPU也使用一个配对的内存屏障。
- 不能保证某些 CPU片外硬件不会重排对内存的访问。CPU缓存一致性机制将在CPU之间传播内存屏障的间接影响,但是不会按顺序执行这项操作。
内存屏障必须成对使用才能推导出先后顺序。
当处理 CPU-CPU 交互时,几种类型的内存屏障总是应当配对。缺少适当的配对将总是会产生错误。一个写屏障应当总是与数据依赖屏障或者读屏障配对,虽然通用屏障也是可以的。类似的,一个读屏障或者数据依赖屏障总是应当与至少一个写屏障配对使用,虽然通用屏障也是可以的。
CPU使用MESI协议(或其升级款)来保证其缓存的一致性。当CPU进行数据写入时,如果数据不在当前核心,那么会发送Read Invalidate消息,指示其它核心将此数据置为无效状态并发送给当前核心。
直接参考书中附录B
写内存屏障作用于store buffer,读屏障作用于无效队列,完整的屏障同时作用于两者
- 每一个CPU总是按照编程顺序来感知内存访问
- 仅仅在操作不同地址时,CPU才对特定的存储操作进行重新排序
- 一个特定 CPU在内存屏障之前的所有装载操作(smp_rmb())将被所有随后的读内存屏障后面的操作之前被所有CPU所感知
- 所有在写内存屏障之前的写操作 (smp_wmb()) 都将比随后的写操作先感知
- 所有在内存屏障之前的内存访问 (装载和存储) (smp_mb())都将比随后的内存访问先感知
精度问题出现的本质原因是浮点数无法精确的表示大多数十进制小数,以下面的计算为例:
System.out.println(81.6 * 10);
System.out.println(81.6 * 100);
其输出结果是:
816.0
8159.999999999999
数字81.6在写出来之后就不是精确表示了,随后的计算所以也随之不准确。下面的除法是准确的:
System.out.println(816 / 100D);
因为对于整数,816和100都是可以精确表示的,所以最后的结果便是对的。
在 #11 的基础上,把代码再改写为:
package test;
class MultiProcessorTask {
private boolean flag = true;
public void runMethod() {
while (flag) {
System.out.println(1);
}
}
public void stopMethod() throws InterruptedException {
System.out.println("准备睡眠1秒,然后置flag为false.");
Thread.sleep(1000);
System.out.println("change 'flag' field ...");
flag = false;
}
}
class ThreadA extends Thread {
private MultiProcessorTask task;
ThreadA(MultiProcessorTask task) {this.task = task;}
@Override
public void run() {
task.runMethod();
}
}
public class TestRun {
public static void main(String[] args) throws InterruptedException {
MultiProcessorTask task = new MultiProcessorTask();
ThreadA a = new ThreadA(task);
a.start();
task.stopMethod();
System.out.println("it's over");
}
}
这样也能正常退出,众所周知,打印里面是有锁的,所以这里是打印还是锁阻止了JIT激进优化?不知道,对JIT优化的过程了解很少,我赌五毛是打印。
package test;
public class TestRun {
private static int version = 0;
private static class Writer implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(50);
version += 1;
System.out.println("Writer更新为: " + version + ", 时间: " + System.currentTimeMillis());
} catch (InterruptedException ignore) {
}
}
}
}
private static class Reader implements Runnable {
private final int index;
private final long sleepTime;
private Reader(int index, long sleepTime) {
this.index = index;
this.sleepTime = sleepTime * 10;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(sleepTime);
System.out.println("Read" + index + "读到: " + version + ", 时间: " + System.currentTimeMillis());
} catch (InterruptedException ignore) {
}
}
}
}
public static void main(String[] args) {
new Thread(new Reader(1, 3)).start();
new Thread(new Reader(2, 4)).start();
new Thread(new Writer()).start();
}
}
一个写线程每隔50毫秒把version
加一,两个读线程分别每隔30毫秒和40毫秒读一次version
值,测试执行了147089次,触发了最高级别的C2/OSR/Level 4编译,在这种情况下仍可以保证Reader线程读到最新值,如下:
Writer更新为: 147087, 时间: 1593965931595
Read1读到: 147087, 时间: 1593965931603
Read2读到: 147087, 时间: 1593965931612
Read1读到: 147087, 时间: 1593965931635
Writer更新为: 147088, 时间: 1593965931646
Read2读到: 147088, 时间: 1593965931656
Read1读到: 147088, 时间: 1593965931665
Read1读到: 147088, 时间: 1593965931698
Writer更新为: 147089, 时间: 1593965931698
Read2读到: 147089, 时间: 1593965931698
Read1读到: 147089, 时间: 1593965931733
Read2读到: 147089, 时间: 1593965931739
编译级别:
至此彻底说明了,对于单个变量的并发读写在硬件层面上根本无需同步,给我们造成"不可见"现象的真正原因是JIT的激进优化。当然,JMM规范还是应当遵守的,毕竟不能保证所在的场景就一定不会被JIT做些手脚。这几个例子的用意是说清楚长久以来在🧠里的一些混乱。
代码示例:
public enum StatusEnum {
CREATING(0),
PUBLISHED(1),
FINISHED(2);
private final int status;
StatusEnum(int status) {
this.status = status;
}
public int getStatus() {
return status;
}
private static final Map<Integer, StatusEnum> mapper = new HashMap<>(3);
static {
Arrays.stream(StatusEnum.values()).forEach(
statusEnum -> mapper.put(statusEnum.getStatus(), statusEnum)
);
}
/**
* 将status值转为枚举.
*/
public static StatusEnum fromStatus(final Integer status) {
if (status == null) {
return null;
}
return mapper.get(status);
}
}
枚举的实现原理是:
public static final
的字段fromStatus
的调用导致StatusEnum类加载,而类加载后枚举字段已被初始化完毕,所以这样是行得通的A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.