本文有点长,请慢慢食用…(当然想更清楚还是去看上次推荐的书)
Java 内存模型(JMM)
JMM的抽象示意图:
由图可知:
- 所有的共享变量都存在主内存中。
- 每个线程都保存了一份该线程使用到的共享变量的副本。
- 如果线程A与线程B之间要通信的话,必须经历下面2个步骤:
a. 线程A将本地内存A中更新过的共享变量刷新到主内存中去。
b. 线程B到主内存中去读取线程A之前已经更新过的共享变量。
因为根据JMM的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。所以,线程A无法直接访问线程B的工作内存,线程间通信必须经过主内存。说人话就是:线程A操作的结果对线程B是不可见的,必须要等结果刷新回主存才变成可见的变量。
JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证。
重排序与happens-before
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。
指令重排一般分为以下三种:
- 编译器优化重排
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 - 指令并行重排
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。 - 内存系统重排(导致了内存可见性的问题)
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
JMM提供了happens-before规则(JSR-133规范),满足了程序员的需求——简单易懂,并且提供了足够强的内存可见性保证。换言之,程序员只要遵循happens-before规则,那他写的程序就能保证在JMM中具有强的内存可见性。
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。happens-before关系的定义如下:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。
总之,如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的,不管它们在不在一个线程。
volatile关键字呢由于本人已经挺熟悉的了,就不写了,想了解的小伙伴就自行去看书吧…
内存屏障
JVM通过内存屏障来实现限制处理器的重排序。
什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。(注意这里的缓存主要指的是CPU缓存,如L1,L2等)
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:
- 在每个volatile写操作前插入一个StoreStore屏障;
- 在每个volatile写操作后插入一个StoreLoad屏障;
- 在每个volatile读操作后插入一个LoadLoad屏障;
- 在每个volatile读操作后再插入一个LoadStore屏障。
synchronized与锁
synchronized底层原理
首先需要明确的一点是:Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁。
我们通常使用synchronized关键字来给一段代码或一个方法上锁。它通常有三种形式:
synchronized
在实例方法上,锁为当前实例synchronized
在静态方法上,锁为当前Class对象(即类模板)synchronized
在代码块上,锁为括号里面的对象
public void blockLock(