并发编程三大特性:可见性、原子性、有序性
volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制
多核并发缓存架构:
Java线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
以下测试程序,测试线程会在工作内存中保存一个共享变量的副本。
package demo6;
public class VolatileTest {
private static boolean initFlag = false;
public static void main(String[] args)throws InterruptedException {
new Thread(new Runnable(){
public void run() {
System.out.println("waiting data ....");
//死循环
while(!initFlag) {
}
System.out.println("*************sucess!!!");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
public void run() {
prepareData();
}
}).start();
}
public static void prepareData() {
System.out.println("prepareing data .....");
initFlag = true;
System.out.println("prepare data end.....");
}
}
打印如下:
由上打印结果可知在prepareData()方法中initFlag的值改变并未影响另一个线程while(initFlag)的值。这是由于多个线程中都有一个副本值,当另一个线程修改了这个值后对其它线程是不可见的。
解决办法:(1.用 volatile 修饰2.synchronized(也会刷新副本中工作内存中的值))
private static volatile boolean initFlag = false;
所以:volatile 作用之一: 用来修饰共享变量可以达到数据在多线程之间的可见性,就是一个线程修改了这个变量,其它线程知道它修改了。
方法:使用的是MESI缓存一致性协议
JMM数据原子操作
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入工作内存
- use(使用):从工作内存读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存数据写入主内存
- write(写入):将store过去的变量赋值给主内存中的变量
- lock(锁写):将主内存变量加锁,标识为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
下面我们来看一下上面加了volatile 关键字后代码在硬件中原子操作的运行流程(JMM内存模型):
volatile 缓存可见性实现原理:
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存
IA-32架构软件开发者手册对lock指令的解释
- 会将当前处理缓存行的数据立即写回到系统内存
- 这个写回操作会引起在其它cpu里缓存了该内存地址的数据无效(即MESI协议)
我们来看一下volatile不支持的原子性
package demo6;
public class VolatileTest1 {
public static volatile int num =0;
public static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for(int i=0;i<threads.length;i++) {
threads[i] = new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<1000;i++) {
increase();
}
}
});
threads[i].start();
}
for(Thread t:threads) {
t.join();
}
System.out.println(num);
}
}
如果volatile保证原子性则打印结果应为10000
但结果打印如下:
每一次打印的结果都不一样,这是因为其它线程同时也做自增操作时从新去主存read时,原来自增的值会丢失(出现了写覆盖)。
解决办法:1.加synchronized(2.AtomicInteger 封装类)
package demo6;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileTest1 {
public static volatile int num =0;
public static void increase() {
num++;
}
static AtomicInteger atomicInteger = new AtomicInteger();
public static void addMyAtomic() {
atomicInteger.getAndIncrement(); //每次自增1
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for(int i=0;i<threads.length;i++) {
threads[i] = new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<1000;i++) {
increase();
addMyAtomic();
}
}
});
threads[i].start();
}
for(Thread t:threads) {
t.join();
}
System.out.println("不保证原子性:"+num);
System.out.println("保证原子性:"+atomicInteger);
}
}
打印结果为:
本博文内容出自原作者链接