并发-4-volatile

176 阅读6分钟

内存模型的相关概念:

程序的执行过程为:主存->复制到cache(CPU的高速缓存)->CPU->刷新cache->回写到主存中 共享变量:被多个线程同时访问的变量 特殊情况:主存中i=0,程序A和B分别读i=0至核心1和核心2(多核CPU)的cache,进行i=i+1,则对这个共享变量虽然操作了两次,但是最后写回主存还是i=1。具体过程如下:

    |-------主存-------|----核心1的cache----|----核心2的cache----|

    |-------i=0-------|-----i=0-----------|------i=0-----------|

    |-------i=0-------|-----i=1-----------|------i=1-----------|

    |-------i=1-------|-----i=1-----------|------i=1-----------|

这就是著名的缓存一致性问题 问题的解决方案MESI协议:当CPU写数据时,如果发现这个变量是共享变量,则通知其他的CPU设置该共享变量的缓存行为无效,当其他CPU需要读取这个变量时,发现自己的缓存中缓存该变量的缓存行是无效的,它就会从内存中读取

原子性:

要么全部执行,要么全部不执行,不会被打断。
eg.对一个32位的int型变量赋值:
eg.i=3分为两步:1.对低16位赋值。2.对高16位赋值。这时就必须要原子性操作,要么全部成功,要么全部不成功

可见性:

多个线程访问同一个变量的时候,一个线程修改了值,另一个线程能立即看到修改的值
eg.线程1:int i = 0;    i=10,    线程2:j=i。
CPU1执行线程1,CPU2执行线程2
当线程1将i=0读到cache中并且设置为10(线程修改了值),但是未写入主存,线程2执行j=i时还是取出主存中i=0的值(未看到最新的值)

有序性:

以下指令可能会发生指令重排序

语句1:int a=10;

语句2:int r=2;

语句3:a=a+3;

语句4:r=a*a;

考虑数据依赖性之后的指令执行顺序可能是2->1->3->4

多线程下的有序性问题:

线程1:(语句1)context = loadContext();   

      (语句2)intited=true;

线程2:(语句1)while(!inited){

           sleep()

       }

      (语句2)doSomethingwithconfig(context)

因为线程1的两个语句之间没有依赖,所以可能发生不恰当的指令重排列:如下:

线程1先执行了语句2,导致线程2的intied误认为初始化完成,线程2跳过语句1,进行了语句2的doSomethingwithconfig(context)工作

由此可见,指令重排列不会影响当个线程的执行,但会影响到程序并发的执行

java内存模型:

在JVM规范中,试图定义了一种Java内存模型,(java memeory model,JMM)来保证各个平台下的内存访问差异,以实现个平台下的JVM都能达到一致的访问内存的效果,但是JVM没有限制CPU或者寄存器,更没有禁止指令重排列,所以也会存在一致性问题。

Java内存模型规定所有的变量都是存在主存当中(类似于物理内存),每个线程都有自己的工作内存(类似于高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

原子性:java内存模型只对基本数据类型的读取和赋值(int i = 0)保证了原子性,剩下的需要由synchronized和Lock来实现

可见性:volatile修饰的变量,一旦发生修改,就会更新主存,synchronized和Lock一样可以保证可见性

synchronized和Lock可以保证在释放锁之前将会对变量的修改刷新到主存中

案例分析:

volatile的意义:

    1.保证了不同线程对这个变量进行操作的可见性    

    2.禁止指令重排序
//线程1
boolean isStop = false;
while(!stop){
   doSomething()
}
//线程2
stop = true;
每个线程都有自己的工作内存,每个线程对变量的操作都要在工作内存中进行,不能直接对主存进行操作,线程之间的工作内存相互隔离

线程1将isStop读取到自己的工作内存,如果线程2在自己的工作内存中对isStop进行了修改,但是线程1还是没有进行刷新,所以会一直运行下去

对线程1的isStop加上volatile之后就保证了可见性:

1.当线程2修改isStop时,会做两件事,一就是对isStop立即更新到主存,二就是置线程1的缓存行为无效

2.当线程1再次读取isStop时,发现自己的缓存行无效,就会去读主存最新的值

volatile可以保证共享变量的可见性

案例分析2:

public class ThreadVolatile {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final ThreadVolatile test = new ThreadVolatile();

        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            }).start();
        }

        while (Thread.activeCount() > 1) {
            //保证前面的线程都执行完
            Thread.yield();
        }
        out.println("inc=" + test.inc);
    }
}

输出:

inc=999452
因为自增操作不是原子性的,所以虽然保证了可见性,但还是不够,每次操作的数都会小于10000

eg.假设i=10此时,线程1取出i=10,还未进行(i=i+1,写入主存)这两个操作时就阻塞了

   线程2取出i=10,完成了i=i+1,写入了主存i=11

   线程1执行i=i+1,写入主存i=11;

volatile不可以保证原子性

使用AtomicInteger,是JDK中新增的一种利用CAS锁原理实现的基本数据类型的原子操作

参考下面的代码:
public class ThreadVolatile {
    public AtomicInteger anInt = new AtomicInteger(0);

    public void increase() {
        anInt.incrementAndGet();
    }

    public static void main(String[] args) {
        final ThreadVolatile test = new ThreadVolatile();

        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            }).start();
        }

        while (Thread.activeCount() > 1) {
            //保证前面的线程都执行完
            Thread.yield();
        }
        out.println("inc=" + test.anInt.get());
    }
}

输出:

inc=1000000

有序性:

因为volatile不可以保证变量的原子性,但是可以保证变量的可见性,那么可以部分保证有序性,如下:
//线程1:
context = loadContext();   //语句1
volatile inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

使用场景:

具备条件:

1.对变量的写操作不依赖于当前值

2.该变量没有包含在其他变量的不变式中

也就是说,必须保证操作是原子性操作(i++就不可以),才能保证volatile关键字的程序在并发的时候能够正确执行

例如: 状态标记量:

volatile boolean flag = false;
while(!flag){
    doSomething()
}
public void setFlag(){
    flag = true
}

//使用于double check机制

class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {
    }
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}