并发过程的三大特征
volatile是java中的一个关键字.说到volatile就不得不介绍三个相关的概念.
可见行:可见性指的是当一个线程修改了共享变量的值之后,其他的线程能够立即知道.java内存模型是通过在变量修改后将新值同步回主存中,在变量读取前从主内存刷新变量这种依赖主内存作为传递媒介的方式来实现可见性的.无论普通变量还是volatile变量都是这样实现的.而他俩的区别是volatile的特殊规则保证了新值能够立即同步到主内存,以及每次使用前立即从主内存刷新.而实现这种效果的方法还有:final和synchronize.
原子性:是指一组操作,要么全执行要么全不执行.在java中,read、load、assign、user、store和write的原子性是由JMM(java内存模型)直接保证的.我们可以大致认为,基本数据类型的访问、读写都是具备原子性的(例外就是long和double的非原子协定.)另外lock和unlock、monitorenter和monitorexit字节码指令能够更大范围的保证原子性.而其中monitorenter和monitorexit这两个字节码指令反应到java中就是synchronize关键字.
有序性:有序性对应的就是指令重排序,为什么会出现指令重排序呢?主要是因为编译器/处理器对用户执行代码的一种优化手段,在一定的规则下,允许将多条指令不按程序定义的顺序执行,以达到更高的执行效率.
volatile保证可见性
volatile是怎么保证可见性的呢?
每个工作的线程都有自己的工作内存,线程内部的资源相互隔离,线程属于进程会共享进程的一部分资源.cpu资源的最小调度单位是线程,而系统资源比如内存的最小分配单位是进程.当线程想要操作共享变量值的时候,会先从共享内存中load到自己的工作内存,然后进行use,但是此时并没有直接写回内存中,如果这个时候其他线程需要读取或者操作这个变量的时候,读取到的数据就不是最新的内容,造成数据的不可见.
package cn.bugstack.springframework;
public class VolatileTest {
public static void main(String[] args) {
T1 t1 = new T1();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException ignore) {
}
t1.setFlag(true);
System.out.println("大白猫咪!");
}
});
thread1.start();
thread2.start();
}
}
class T1 implements Runnable{
private Boolean flag = false;
public Boolean getFlag() {
return flag;
}
public void setFlag(Boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while (!flag){
}
System.out.println("小黑猫咪!");
}
}
执行结果
上面这段代码按照正常逻辑,当线程2设置flag=true的时候,线程1退出循环,并输出“小黑妈咪!”,但是执行的结果却是线程1一直没有退出循环,这说明当线程2改变了线程1中的变量,但是线程1并没有去获取最新的值.
变量flag加上volatile修饰.
package cn.bugstack.springframework;
public class VolatileTest {
public static void main(String[] args) {
T1 t1 = new T1();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException ignore) {
}
t1.setFlag(true);
System.out.println("大白猫咪!");
}
});
thread1.start();
thread2.start();
}
}
class T1 implements Runnable{
private volatile Boolean flag = false;
public Boolean getFlag() {
return flag;
}
public void setFlag(Boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while (!flag){
}
System.out.println("小黑猫咪!");
}
}
执行结果
被volatile关键字修饰的变量,每次在使用前,都需要从内存中重新read、load,而在每次修改后,都需要store、write操作,这样不管是哪个线程对变量进行了操作,都会拿到最新的值到自己的工作内存中.
volatile保证有序性
普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序执行的顺序一致.因为在同一个线程的方法执行过程中无法感知到这点,这也就是java内存模型中描述的所谓“线程内表现为串行的语义”.
package cn.bugstack.springframework;
import java.util.HashMap;
import java.util.Map;
public class VolatileTest2 {
Map configOptions;
char[] configText;
// 这里必须使用volatile修饰
volatile boolean initialized = false;
// 假设以下代码在线程A中执行
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigoptions(configText,configOptions);
initialized = true;
// 假设以下代码在线程B中执行
// 等待initialized为true,代表线程A已经把配置文件信息初始化完成
while(!initialized){
sleep();
}
// 使用线程A初始化好的配置信息
doSomethingWithConfig();
}
如果没有使用volatile修饰的话,就可能会由于指令重排序,线程A先执行了initialized = true;,导致线程B执行错误.所以需要加上volatile关键字.
为什么加上volatile关键字之后,就能禁止指令重排序了呢?
其实java程序编译之后,加上volatile关键字的程序,会多一个lock addl $0x0,(%esp)(把ESP寄存器的值加0)操作,这个操作的作用相当于一个内存屏障:指重排序时不能把后面的指令重排序到内存屏障之前的位置.他的作用是将本处理器的缓存写入内存,该写入动作也会引起别的处理器或者别的内核无效化其缓存,这种操作相当于对缓存中的变量做了一次JMM中所说的store和write操作.
lock addl $0x0,(%esp)指令把修改同步到内存中的时候,意味着所有的之前的操作都已经完成了,这样就形成了“指令重排序无法越过内存屏障”的效果.不会出现像上面说到的只提前执行了initialized = true,并且刷新到主存中去.而使得线程B错误执行.
volatile不能保证原子性
虽然volatile可以保证可见性但是并不能保证原子性.虽然每次volatile的变量都是最新的,但是当执行修改操作的时候,例如i++操作,看上去是一个指令,而被编译之后,字节码指令确实三个,先load,再add,最后store.如果是多线程的时候执行,就会导致load到内存中的新数据,但是当操作add的时候,别的线程已经修改了i的值并且store回了内存中,这个时候再add就会产生错误.
package cn.bugstack.springframework;
public class VolatileTest3 {
private static volatile int i = 0;
public static void main(String[] args) {
for (int j = 0; j < 10; j++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int k = 0; k < 1000; k++) {
i++;
}
}
}).start();
}
System.out.println("i=" + i);
}
}
运行结果