volatile关键字与内存可见性

一、volatile关键字与内存可见性

1、测试没有 volatile关键字的demo

public class VolatileTest1 {

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();
        while(true){
            if(threadDemo.isFlag()){
                System.out.println("----主线程读到flag为true----");
                break;
            }
        }
    }
}

class ThreadDemo implements Runnable {
    private boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
        }
        flag = true;
        System.out.println("子线程修改了值flag=" + isFlag());
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

     

在子线程中将线程的共享变量 flag的值修改成了 true时,但是主线程在条件判断时读到的flag一直是false,所以while循环不会停止跳出,程序不会终止。这是由于内存的可见性导致的。

2、内存可见性(Memory Visibility)

内存可见性(Memory Visibility)其实是指共享变量在不同线程之间的可见性。

  • 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量,即通常称这种被多个线程访问的变量为共享变量。
  • 可见性:指当某个线程正在使用共享变量并对共享变量的值做了修改时,能够及时的被其他线程看到共享变量的变化。

 

内存可见性与Java内存模型有关系

所有的变量都存储在主内存中(操作系统给进程分配的内存空间),而每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本。

    volatile关键字与内存可见性_第1张图片

注意:线程对共享变量的所有操作都必须在自己的工作内存(working memory,是cache和寄存器的一个抽象,并不是内存中的某个部分)。不同线程之间,当前线程无法直接访问其他线程的工作内存中的变量,线程间变量值得传递需要通过主内存来完成。

缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
 

 

解决共享变量的内存可见问题的方式有很多

1、synchronized实现可见性

JMM(Java内存模型)关于synchronized的两条规定:

线程解锁前,必须把共享变量的最新值刷新到主内存中,

线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();
        while(true){
            // main线程加锁
            synchronized(threadDemo){
                if(threadDemo.isFlag()){
                    System.out.println("----主线程读到flag为true----");
                    break;
                }
            }
        }
    }

2、volatile关键字实现可见性

对于多线程, volatile不具备“互斥性”,不能保证变量状态的“原子性操作”。

使用 volatile 关键字用来确保将变量的更新操作通知到其他线程。

某个线程的工作内存中修改了共享变量的值并会刷新到主内存中,同时其他线程已经读取的共享变量副本就会失效,需要读数据时就会再次去主内存中读取新的共享变量的值,从而达到共享变量内存可见。

 // 共享变量用 volatile修饰即可   
 private volatile boolean flag = false;

    

synchronized 和 volatile比较

synchronized具备“互斥性”,既能保证可见性,又能保证原子性,volatile不具备“互斥性”,只能保证可见性,不能保证原子性。

volatile不需要加锁,比synchronized更轻量级,不会阻塞线程,效率更高。如果能用 volatile解决问题,应尽量使用volatile,因为它的效率比synchronized更高。

 

二、原子性

原子性:一次操作,要么全部执行成功,要么全部执行失败。一个很经典的例子就是银行账户转账问题。

1、一个实例demo

public class AtomicTest {
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo(0);
        for (int i = 0; i < 10; i++) {
            new Thread(atomicDemo).start();
        }
    }
}

class AtomicDemo implements Runnable{
    //线程共享变量
    private volatile int number;

    public AtomicDemo(int number) {
        this.number = number;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName() + ",number=" + ++number);
    }
}

       volatile关键字与内存可见性_第2张图片

运行结果会发现可能会在不同的线程中,看到相同的数值,这是由于 volatile关键字保证了操作的内存可见性,但是 volatile不能保证操作的原子性。

自增操作不是原子性操作,它包括读取变量的原始值、进行加1操作、写入工作内存。而且volatile也无法保证对变量的任何操作都是原子性的。

2、解决原子性操作问题--JUC

java.util.concurrent.atomic 原子操作类包里面提供了一组原子变量类。封装了一系列常用的数据类型对应的封装类,

Java.util.concurrent.atomic 中实现的原子操作类可以分成4组:

标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

复合变量类:AtomicMarkableReference,AtomicStampedReference

这些类都保证了两点:

1)类里的变量都用了volatile保证内存是可见的

2)使用了一个算法CAS(Compare And Swap),保证对这些数据的操作具有原子性

public class AtomicTest1 {

    public static void main(String[] args) throws InterruptedException {
        //线程共享变量
        AtomicInteger atomicInteger = new AtomicInteger(0);
        AtomicDemo1 atomicDemo = new AtomicDemo1(atomicInteger);
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(atomicDemo);
            thread.start();
        }
    }
}

class AtomicDemo1 implements Runnable{
    private AtomicInteger atomicInteger = null;

    public AtomicDemo1(AtomicInteger atomicInteger) {
        this.atomicInteger = atomicInteger;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName() + ",atomicInteger=" + atomicInteger.incrementAndGet());
    }
}

      volatile关键字与内存可见性_第3张图片

 

参考文章:

Java并发编程:volatile关键字解析

阿里面试:跟我死磕Synchronized底层实现,我满分回答拿了Offer

你可能感兴趣的:(Java)