volatile关键字:这里介绍对线程有可见性与有序性
有序性是指A线程在改变某个属性值是其他线程也能够看到同一个属性的变化.也许在单线程的环境不会有这个问题,但是多线程比较特殊.这里借用别人的图可以清晰看到他们的关系.
A线程与B线程操作的是本地内存,而不是直接操作主内存.比如当A线程改变了某个属性值改变的是本地内存A他没有刷新到主内存,当B线程操作该属性读取到的还是老的值.
导致内存不可见的问题.volatile关键字可以通过内存屏障解决该问题.
他让当A线程改变了某个属性值立马刷新到主内存里,而让本地内存失效,当B线程操作该属性读取他时只能到主内存里读取.这样就实现了内存的可见性.
代码演示
package father.volatileTest;
import java.util.concurrent.atomic.AtomicInteger;
class MyData{
static int number = 0;
Object object = new Object();
public void add(){
this.number = 66;
}
public void addPlusPlus(){
this.number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
}
class VolatileDemo {
public static void main(String[] args) {
seeByVolatile();
}
//验证可见性的方法
public static void seeByVolatile() {
MyData myData = new MyData();
//第一个线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.add();
System.out.println(Thread.currentThread().getName() + " update number to " + myData.number);
}).start();
//第二个线程 main
//System.out.println(MyData.number);
while (myData.number == 0){
}
System.out.println(Thread.currentThread().getName() + "mission is over");
}
}
不使用volatile结果会一直卡在这两行
验证volatile的可见性1.当number未被volatile修饰时,new Thread将number值改为66,但main线程并不知道,会一直在循环中出不来
2.当number使用volatile修饰,new Thread改变number值后,会通知main线程主内存的值已被修改,结束任务。体现了可见性
2.有序性
指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
当单线程时
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序
当多线程时
package father.volatileTest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Ordering{
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < Long.MAX_VALUE; i++) { // 超大次数循环
// 重置数据
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(2);
// 线程一
Thread t1 = new Thread(() -> {
a = 1;
x = b;
latch.countDown();
});
// 线程二
Thread t2 = new Thread(() -> {
b = 1;
y = a;
latch.countDown();
});
t1.start();
t2.start();
latch.await();
//
if (x == 0 && y == 0) {
System.err.println("第" + i + "次循环出现(" + x + "," + y + ");");
break;
}
}
}
}
出现下面结果说明他指令乱序了
那CPU层面如何禁止重排序呢?
内存屏障,对某部分内存操作时前后添加屏障,屏障前后的操作不可以乱序执行。
JVM是通过对volatile关键字修饰的对象前后添加屏障实现禁止指令重排序的,在volatile写操作前后加StoreStoreBarrier,在volatile读操作前后加LoadLoadBarrier。
另外JVM规定重排序必须遵守8条hanppens-before原则。
进一步理解
代码示例
单例DCL的代码
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "构造方法");
}
//DCL双端加锁机制
public static SingletonDemo getInstance(){
if (instance == null){
synchronized (SingletonDemo.class){
if (instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
}
instance = new SingletonDemo();; 分为一下三步
memory = allocate() //分配内存
ctorInstanc(memory) //初始化对象
instance = memory //设置instance指向刚分配的地址
2 ,3 步不存在数据依赖, 可以指令重排的执行顺序为 1 ,3 ,2,设置instance指向刚分配的地址,次数instance还没有初始化完
但此时instance不为null了,若正好此时有一个线程来访问,就出现了线程安全问题
指令跟像是instance实例化的一个步骤这样在单线程时指令重排序了但结果还是不受影响但多线程
在instance还没有初始化完访问就会有问题.