并发(1)-volitale、synchronized、final内存语义
2021-01-05 01:22:29    258    0    0
pop6

对于原子操作的解释:不可中断的一个或者一组操作。

单核环境下:原子操作仅仅是不可中断的一个或者一组操作,因为只有一个CPU在执行指令。

多核环境下:原子操作不仅不能被中断,并且该CPU执行原子操作时其他CPU不能读写该操作访问的内存地址。

 

在多核下,CPU实现原子操作的方式有:(1)锁缓存(2)锁总线

首先CPU会自动保证基本内存操作的原子性(多个线程读/写时,为了保证安全,我们还要保证其可见性和有序性),即read和write,它们是原语。当一个CPU进行read或者write操作时,其他CPU不能访问读/写字节的内存地址。

 

内存屏障:内存屏障前后的指令不会重排序,即内存屏障之前的指令必定将在内存屏障之后完成。Java的volatile关键字会在使用变量的地方插入内存屏障(根据对volatile变量的读写情况分别在前、后插入)。

 

但是单线程,因为数据依赖性的关系,CPU不会重排序存在依赖性的操作,所以无需插入内存屏障,即volatile。所有单线程程序无需保证可见性和有序性。

 

 

**

MESI协议所做的:MESI协议是为了保证CPU缓存之间的一致性的协议,MESI分别是4个状态。CPU的cache通过嗅探总线上的数据从而更改自己缓存的状态。

 

例如:

CPU1,CPU2的缓存行都缓存了数据a。

此时CPU1修改了a变量并放入自己的缓存,而CPU2通过嗅探总线从而将自己cache的状态修改为invalid。

 

**

对于单个变量的读/写,CPU自动保证原子性,即当一个处理器操作该变量时,其他处理器不能访问该变量的内存地址。这是通过锁缓存或者锁总线实现的,即当一个CPU试图修改某个变量时,它会将缓存锁定,在MESI协议的影响下,它会使得其他CUP不能访问缓存了该变量的缓存行。即不可能存在同时对一个变量的读、写、写/读、读/写。                                 

 

线程同步要保证的特性:

1.原子性(读-改-写使用锁组合成原子操作)

2.可见性(例如CPU1更新变量a到Cache,CPU2度变量a会从内存读,就会导致数据不一致)

3.有序性(插入内存屏障防止指令重排序)

 

volatile两个重要特性:

1. 可见性,volatile变量的写会立马回写到内存

2. 有序性:JVM会根据volatile变量的读、写前后插入内存屏障,禁止指令重排序。

因为volatile是修饰单个变量的,而单个变量的读/写本身就是原子的

 

可以说,valtile是轻量级的synchronized。

 /**
 * volatile好比对变量的单个读/写操作做了同步
 * 
 * 如下代码,操作volatile变量的方法和对这些方法加上synchronized关键字的效果相同。
 * 因为CPU摆正单个的读/写操作是原子的,而单个读/写不牵扯到指令重排序,并且volatile保证了变量的可见性,
 * 而synchronized的hp规则也保证了变量的可见性。所以效果相同。
 */
public class VolatileTest {
 private volatile int a;
 
 public void setA(int a) {
    this.a = a;
 }
 
 public int getA() {
    return a;
 }
}​ 

 

volatile可见性的例子:

CPU1缓存行缓存了变量a。接着CPU1修改了变量a并放回到缓存中。

CPU2的缓存未缓存变量a。CPU2需要读取变量a的值,它从内存读取。


使用volatile的时机:

当某个线程对共享变量的修改需要对其它线程可见时,可以套用volatile的hp关系。

 

hp规则:

hp规则阐述操作之间的内存可见性。如A hp B,无论这两个操作是在单线程内,还是多线程内,hp规则都能保证A操作对B操作可见,而实际的实现是通过插入内存屏障来禁止指令重排序实现的。hp规则简化了程序员对指令之间重排序的判断,无需程序员去牢记复杂的重排序规则,以及内存屏障的插入位置。

其中volatile的hp规则简化了对volatile变量内存屏障插入位置的判断

插入内存屏障后,会使得内存屏障之前的指令执行的结果必定于之后的指令可见,这比如双检锁的问题。


常见hp规则:

1. 线程内规则:一个线程的每个操作 hp 于其后的操作

 

2. synchronized规则:unlock hp lock

 

3. volatile规则:写 hp 读

 

4. 传递规则:若a hp b, b hp c, 则 a hp c。

 

5. start()规则:在a线程启动b.start()操作之前的任意操作 hp b.run()。即b.start()是一个分割线,之前是a线程进行b.start()的任意操作,之后是b的任意操作。

 

6. join()规则:b.run() hp 在a线程执行b.join()后的任何操作。同start()。


注意:hp规则只是保证了操作之间的可见性,而不是说a hp  b,a就必须在b之前执行。例如单线程内:

1 hp 2,但是1不一定在2之前执行。因为1、2之间无数据依赖性,因此CPU或编译器可能会对此重排序为2-1执行。但是这不影响1的执行结果对2可见。

 

可以说,hp规则在程序与和JMM的禁止重排序规则之间搭建了一座桥梁,避免了程序员去学习复杂的重排序规则(JVM根据volatile变量读、写前后操作分情况插入不同类型的内存屏障)。

其实现如下:

是否能重排序   

 

第二个操作

第一个操作普通读/写volatile读volaile写
普通读/写×
volatile读×××
volatile写××

 

程序员可以根据该表去将合适的变量声明为volatile做同步,也可以根据方便的volatile规则去做同步。


-----------------------volatile、synchronized总结-----------------------

volatile是轻量的synchronized(synchronized对单个变量读/写的同步效果和volatile相同)。这也表明synchronized具有volatile的所有特性。而synchronized所具有的这种特性是加锁、解锁提供的,而加锁、解锁是使用CAS替换Java对象头的MarkWord部分实现的。

所以说,synchronized所具有volatile的这些特性是CAS提供的。而volatile和CAS(借用汇编的cmpxchg指令)具有的共同的特性是它们翻译成汇编指令后,都具有一个lock前缀。

 

而lock前缀的指令作用如下:

1. lock修饰的指令会原子的执行

2. 禁止该指令与之前、之后的指令重排序

3. 将写缓冲区的数据回写到内存

 

由此可见synchronized同步的特性:

1. 原子性:获得锁的线程才可执行块内代码

2. 可见性:解锁后线程本地的数据会被刷回内存

3. 有序性:块内指令不会和块前和块后重排序,但块内可能重排序

 



final的内存语义

final的内存语义保证对象的引用在为任意线程可见之前,该引用变量所指向的对象内的final域已经被正确初始化了,这是通过插入内存屏障实现的。而普通变量则无法保证,即在构造方法内对变量的赋值可能被重排序到构造方法外。

例如:

 

public class Test {
	final int a;
	
	public Test() {
		a = 1;
	}
}

 

在这里,即a的初始化不会重排序到构造方法外,因为JVM在a的初始化和构造函数返回之间插入了内存屏障。而普通变量则不一定。

但是,这个效果还需要一个保证:即,this不逃逸,因此要避免this逃逸。

public class Test {
	int a;
	Test t;
	
	public Test() {
		a = 1;      //1
		t = this;   //2
	}
}

 

虽然final变量的初始化不会排序到构造方法之外,但是在构造方法内的指令也是可以被重排序到。而这里的1、2操作无依赖性,是有可能重排的。

上一篇: SpringBoot笔记(9)-异步调用

下一篇: 如何阅读JDK、JVM源代码?

258 人读过
立即登录, 发表评论.
没有帐号? 立即注册
0 条评论
文档导航