对于原子操作的解释:不可中断的一个或者一组操作。
单核环境下:原子操作仅仅是不可中断的一个或者一组操作,因为只有一个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操作无依赖性,是有可能重排的。
没有帐号? 立即注册