并发(一):java并发概念以及Volatile可见性,重排序

xiaoxiao2021-02-28  16

参考:java并发编程的艺术-方腾飞

以自己的思路总结下

1.线程通信与线程同步:

通信:指线程之间以何种机制来交换信息。

a.共享内存模式即:线程之间共享程序的公共状态,通过读写内存中的公共状态来进行间接通信。

b.消息传递模式:线程之间没有公共状态,线程之间必须通过发送消息来直接进行通信。

同步:程序中用于控制不同线程间操作发生相对顺序的机制。

a.共享内存模式中:某个方法或某段代码在线程之间互斥执行。

b.消息传递模式:控制消息传递顺序,发送消息必须在接收消息之前。

JAVA并发采用的是共享内存模型。

2.内存可见性:

可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

原子性:不能被进一步分割的一组操作。(转换成指令时的操作,long i=0L 会被拆成两个32位指令,而不要认为他是一条指令)

缓存一致性协议:参考:http://blog.csdn.net/iter_zc/article/details/40342695

主要就是处理多个处理器处理同一个主存地址的问题。

MESI是一种主流的缓存一致性协议,已经用在Pentium和PowerPC处理器中。它定义了缓存块的几种状态

modified(修改):缓存块已经被修改,必须被写回主存,其他处理器不能再缓存这个块exclusive(互斥):缓存块还没有被修改,且其他处理器不能装入这个缓存块share(共享):缓存块未被修改,且其他处理器可以装入这个缓存块invalid(无效):缓存块中的数据无效

Volatile可见性和原子性:如果对声明了volatile的变量进行写操作,JVM会像处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到

系统内存。每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将

当前处理器的缓存行设置成无效状态,当处理器对这个数据就行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。Volatile变量实现了

缓存一致性协议所以可以保证可见性。

Volatile原子性是对任意单个Volatile变量的读/写具有原子性,但类似于i++这种复合操作不具有原子性(p38)(对64位long/double型变量普通写操作可能

被拆分成两个32位写操作分配到不同的总线事务中执行,此时对这个64位变量的写操作将不具有原子性。而用Volatile修饰的64位变量不会被分配到

同总线事务中执行,这样可以确保即使是64位的long型和double型变量,只要他是volatile变量,对该变量的读/写就具有原子性。)

synchronized原子性与可见性:

a.原子性:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。在同步代码块执行前添加monitorenter,执行后添加monitorexit指令实现。

即互斥访问同步方法或同步代码块。因此可以保证原子性。

b.可见性:锁获取与Volatile读有相同内存语义,锁释放与Volatile写有相同内存语义。

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

当线程获取锁时,JMM会把该线程对应的本地内置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

(疑问;ReentrantLock()对state变量操作时,已经使用了lock()锁,可以保证对state的操作是原子性和可见性为啥还要给这个变量加个volatile?

  解决:对state进行操作时,使用了CAS来对state进行读写操作,添加了volatile关键字可以保证编译器不能对CAS与CAS前面和后面的任意内存操作

重拍序。

(ps:lock前缀指令作用:

1.确保对内存的读-改-写操作原子执行。现在大多使用缓存锁定来保证指令的原子性。

2.禁止该指令与之前和之后的读和写指令重拍序。

3.把写缓冲区中的所有数据刷新到内存中。

Volatile写指令带有Lock前缀。

CAS操作底层指令:cmpxchg带有Lock前缀。ReentrantLock使用了CAS来对state进行读写操作。

3.重排序:单线程执行情况下在不改变程序结果前提下,编译器和处理器为了优化程序性能而对指令序列进行重拍序的一种手段。重排序在

多线程并发情况下可能会有问题(下面双重检查锁例子说明)。重排序要遵守as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),

(单线程)程序的执行结果不能被改变。(重排序只保证单线程不出错就可以,但是重排序在多线程情况下有可能会出错)

Volatile相比synchronized多了禁止重排序的功能,且实现原子性的方式也不同。灵活使用Volatile效率会比synchronized效率高。

4.内存屏障:JAVA编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。

1.LoadLoad Barriers:

2.StoreStore Barriers:

3.LoadStore Barriers:

4.StoreLoadBarriers:

5.Volatile内存屏障:

1.在每个Volatile写操作的前面插入一个StoreStore屏障。保证在Volatile写之前,其前面的所有普通写操作已经对任意处理器可见。

2.在每个Volatile写操作的后面插入一个StoreLoad屏障。避免Volatile写与后面可能有的Volatile写/读操作重排序。

3.在每个Volatile读操作的后面插入一个LoadLoad屏障。禁止处理器把上面的volatile读与下面的普通读写重拍序。

4..在每个Volatile读操作的后面插入一个LoadStore屏障。禁止处理器把上面Volatile读与下面的普通写重拍序。

Volatile比synchronized更轻量级同时还有禁止重排序的能力。以双重检查锁定来说明下Volatile内存屏障好处

多线程情况下延迟初始化:

public class DoubleCheckdLocking { private static Instance instance; public static Instance getInstance(){ if(instance==null){ //1.第一次检查 synchronized(DoubleCheckdLocking){ if(instance==null){ //2.第二次检查 instance=new Instance();//3.问题代码 } } } } }

序号3中:代码可以分为如下3行伪代码。

memory=allocate();//1.分配对象的内存空间

ctorInstance(memory);//2.初始化对象

instance=memory;//3.设置instance指向刚分配的内存地址。

在单线程情况下处理器认为2-3没有依赖关系,所以可以将2-3重拍序,即:

memory=allocate();//1.分配对象的内存空间

instance=memory;//3.设置instance指向刚分配的内存地址。

ctorInstance(memory);//2.初始化对象

那么在执行第二次实例检查时instance有内存地址但是还未初始化完成,此时线程会以为Instance已经实例化完成,将instance返回。当线程获取实例数据时,可能会抛出空指针异常。所以,为了避免重排序导致的问题,可以使用Volatile来修饰instance变量避免重拍序。

(pS:并发包源码实现模式:

1.声明共享变量(state)为volatile.

2.使用CAS的原子调剂更新来实现线程之间的同步。

3.配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

转载请注明原文地址: https://www.6miu.com/read-1400012.html

最新回复(0)