1 并发3大特性
1.1 原子性
一个或者多个操作要么全部执行,要么全部不执行;在java中对基本数据类型的变量的读取和赋值操作是原子性的(64位处理器);i++不是原子操作
原子性如何保证
- synchronized关键字
- Lock锁
- 通过CAS保证
1.2 可见性
多线程中访问同一个变量时,一个线程修改了变量,其他线程立即可以看到修改的值
可见性保证
- volatile关键字
- 内存屏障
- synchronized
- Lock
1.3 有序性
while (true) {
i++; x = 0; y = 0; a = 0; b = 0;
thread1 -> execute{
shortWait 20000 ns
a=1 ;x=b;}
thread2 -> execute{
b = 1; y = a;}
thread1.start;
thread2.start;
thread1.join;
thread2.join;
if x==0&&y==0
print x,y
break;
}
如上伪代码,在运行中可能会出现x,y=0的情况;出现这种情况是重排序导致的
如何保证有序性
- volatile
- 内存屏障
- synchronized
- Lock
2 java内存模型
多线程通信
线程之间的常用通信方式有2种:共享内存和消息传递;其中java就是使用的共享内存模型
多线程同步
线程之间的同步是控制不同线程操作间发生的顺序
关于更多的线程同步相关可以参见线程的与等待通知机制以及JAVA实现的同步框架AQS
2.1 Java内存模型的抽象结构
Java Memory Model
简称JMM, JMM决定了一个线程的共享变量的写入何时对另外一个线程可见;
JMM是一个线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存当中,每个线程都有一个自己私用的本地内存,本地内存中存储了共享变量的副本
JMM本地内存
JMM本地内存是一个抽象的概念,它覆盖了CPU下的缓存,寄存器以及其他硬件和编译器的优化
从下图看,线程1是无法访问线程2的工作内存,线程1和线程2通信步骤:
- 线程1将更新过的变量,刷新到主内存;
- 线程2从主内存中去读取更新过的变量;
主内存与工作内存交互协议
java内存模型定义了8种原子操作:
- lock: 主内存一个变量设置一个线程独占
- unlock:主内存中变量锁释放
- read:主内存传输到线程工作内存,以便工作内存的load
- load: 把read的主内存的变量load到工作内存
- use: 工作内存的变量值传递到执行引擎执行
- assign: 执行引擎赋值给工作内存
- store: 工作内存中濒临传递到主内存,以便主内存wirte
- write: 将store的变量写入到主内存
执行上述操作必须满足以下规则:
- 变量从主内存复制工作内存,必须按顺序执行read load,从工作内存同步到主内存,必须按顺序执行 store write
- 不允许read,load,store,write单独存在
- 不允许一个线程丢弃它最近的assign, 既工作内存改变之后必须同步主内存
- 一个线程没有assign操作,不允许把数据从工作内存同步到主内存
- 一个新的变量只能在主内存中诞生,不允许使用一个未初始化的变量,即一个变量在use,store之前,必须经过assgin和load
- 一个变量同一时刻只能允许一个线程lock,lock可重入,只有unlock相同次数后,变量才解锁
- 如果对一个变量lock,会清空工作内存中的值
- 变量没有被当前线程lock,不允许被unlock
- unlock前,必须先执行store和write,把变量写到主内存
Java可见性实现
1.内存屏障
- synchronized
- Thread.sleep(10)
- volatiled
lock addl $0x0, (%rsp)
2.cpu上下文切换
- Thread.yield
- Thread.sleep(0)
2.2 锁的内存语义
- 当线程获取锁,JMM会将线程对应的本地内存置为无效
- 当线程释放锁时,JMM会将线程对应的本地内存刷新到主内存
2.3 volatile内存语义
volatile写: JMM会将线程对应的本地内存中的共享变量刷新到主内存
volatile读:JMM会将线程对应的本地内存置为无效,线程会从主内存中获取共享变量
volatile内存语义的实现原理
JMM是语言级的内存模型,确保在不同编译器和不同处理器平台之上,通过禁止特定类型的编译器重排和处理器重排序,为开发者提供一致的内存可见性保证
volatile禁止重排序规则
为了实现其内存语义,JMM会限制编译器重排序
- 第2个操作是volatile写,不管第一个操作是什么,都不能重排
- 第一个操作是volatie读,不管第二个操作是什么,都不能重排
- 第一个操作是volatile写,第2个是volatile读,不能重排
JMM内存屏障
- volatile写前插入 StoreStore, volitile写后插入StoreLoad
- volatile读后边插入 LoadLoad, LoadStore
上述策略比较保守,可以根据不同处理器内存模型继续优化;例如x86不会对读读,读写,写写做重排序操作,所以在x86处理器会省略这3类操作的内存屏障,仅会对写-读操作做重排序
x86的内存屏障指令
- lfence, Load Barrier 读屏障
- sfence, Load Barrier 写屏障
- mfence, 全能屏障,具备lfence和sfence的能力
- Lock前缀,不是内存屏障,可以完成类似内存屏障的功能,Lock会对CPU总线和高速缓存加锁,CPU指令级别的锁
内存屏障有2个能力
- 阻止屏障两边的指令重排
- 刷新处理器缓存
hotspot实现内存屏障
orderAccess_linux_x86.inline.hpp
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
x86处理器中利用lock前缀实现类似的内存屏障的效果
lock前缀指令的作用
- 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
- LOCK前缀指令会禁止该该指令与前面后边的读写指令重排序
- LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效
2.4 happens-before
JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以在不同的线程之内。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
happens-before规则
- 锁定规则:unlock happens-before于lock
- 顺序规则:线程任意操作 happens-before于线程随后的操作
- volatile变量规则:对一个volatile变量的写操作,happens-before于任意后续对这个volatile变量的读操作;
- 传递规则:a happens-before b, b happens-before c,那么a就happens-before于c
- 线程启动规则:a线程start b线程 start happens-before于线程b中任意操作
- 线程中断规则:对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程A执行ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回;
- 对象终结规则:一个对象的初始化完成happens-before于它的finalize()方法的开始。
以上就是jmm对编译器和处理器重排序的约束原则
Java中的volatile关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized关键字不仅保证可见性,同时也保证了原子性(互斥性)。在更底层,JMM通过内存屏障来实现内存 的可见性以及禁止重排序。为了程序员的方便理解,提出了happens-before,它更加的简单易懂,从 而避免了程序员为了理解内存可见性而去学习复杂的重排序规则以及这些规则的具体实现方法。