并发编程的核心
- 原子性
- 可见性
- 有序性
JMM 与三大特性之间的关系
原子性:所有的操作要么全部执行要么不执行
JMM 只保证简单的赋值和获取是属于原子性的(get i,set i ,i=0),Volatile关键字不能保证原子性。Synchronized关键字可以保证原子性,Jdk5以后提供原子类来保证原子性。
可见性:线程之间对共享变量可以感知变化
JMM对于可见性有以下几种方式保证
1.使用Synchronized关键字,保证在同一时刻只有一个线程获取锁执行。
2.使用Volatile关键字,保证多线程的情况下共享变量的操作永远是一致的。下面会具体分析。
3.使用Lock锁,同Synchornized保证同一时刻只有一个线程持有锁执行。
为什么volatile可以保证可见性?
Volatile关键字在Cpu的层面上以及有部分解释,可见性的原因是因为在多个线程内存储了共同一个被Volatile修饰的变量时,当有线程对它进行操作,其他线程的工作内存中该变量置为失效,必须从主内存中获取数据,从而保证可见性。
有序性
JMM对有序性也有如可见性一样的方式。
JMM本身也具备一些有序规则,具体规则如下:
JMM对于代码的执行顺序不一定保证从上至下的顺序,但它会保证结果如预期一致。
int i=2;
int y=4;
y=i+1;
如上结果:y=3,当在执行过程中有可能被JMM按照先执行了“int y=4”,再执行“int i=2”。这种情况称作为“指令重排序”
如果变量被Volatile修饰结果就会保证在执行到Volatile关键字时,前面的代码一定执行完了。
int x=1;
int y=4;
volatile int a=1;
如上:执行过程解释为有可能先“int y=4”->"int x=1"->"int a =1",也有可能先“int x=1”->"int y=4"->"int a =1",
思考:为啥Volatile会保证有序性以及它与JMM之间的关系是如何的?
源码分析Volatile:
从OpenJdk中的unsafe.cpp源码中,发现被Volatile修饰的变量存在一个:“Lock ; ”前缀,
“Lock ;”:前缀实际上是一个内存屏障,会对指令进行以下的保证:
- 确保在指令重排序时不会将其后面的指令放在内存屏障之前。
- 确保在指令重排序是不会将其前面的指令放在内存屏障之后。
- 确保在执行到内存屏障修饰的指令时,前面的指令已经全部执行。
- 强制将线程的工作内存中的值修改刷新值主内存。
- 如果是写指令那么其他线程的工作内存中的值会全部失效
Volatile的使用场景
- 线程的开闭状态利用可见性
- 状态标记 利用顺序性
- Single的double-check设计 利用顺序性
Volatile与Synchornized的区别
使用方式
volatile:只能修饰类变量与实例变量,不可以修饰常量,局部变量,方法,方法参数
synchornied: 不能对变量修饰,只能修饰方法和代码块
volatile修饰的变量可以Null而Synchornied同步代码块的mointer对象不能是Null;
可见性
volatile:是通过是用”Lock ;“ 内存屏障来保存的,强制将其他线程的工作内存的共享变量失效让他从主内存中读取。
synchornied:通过"monitor enter","monitor exit"对其他线程排他,保证同步代码变为串性化,在执行完"monitor exit"会将共享变量都刷新到主内存。
原子性
volatile:不能保证原子性
sync:通过对代码块进行加锁且不能再中途打断,因此保证了原子性。
顺序性
volatile: 通过内存屏障来禁止指令重排序,保证了有序性
sync:通过将同步代码块变为串性化来达到有序性
其他
volatile:不会阻塞线程
sync :其他线程在无法获取锁的情况下会阻塞