1.概述
volatile是Java提供的轻量级的同步机制,保证了可见性,不保证原子性。了解volatile工作机制,首先要对Java内存模型(JMM)有初步的认识:
- 每个线程创建时,JVM会为其创建一份私有的工作内存(栈空间),不同线程的工作内存之间不能直接互相访问。JMM规定所有的变量都存在主内存,主内存是共享内存区域,所有线程都可以访问
 - 线程对变量进行读写,会从主内存拷贝一份副本到自己的工作内存,操作完毕后刷新到主内存。所以,线程间的通信要通过主内存来实现。
 
volatile的作用是:线程对副本变量进行修改后,其他线程能够立刻同步刷新最新的数值。这个就是可见性。

2.可见性验证
我们来看一个例子:
package com.bruceliu.demo15;
import java.util.concurrent.TimeUnit;
/**
 * @BelongsProject: Thread0509
 * @BelongsPackage: com.bruceliu.demo15
 * @Author: bruceliu
 * @
 * @CreateTime: 2020-05-13 23:16
 * @Description: TODO
 */
public class VolatileDemo {
    int x = 0;
    //注意:这里的b没有被volatile修饰
    boolean b = false;
    /**
     * 写操作
     */
    private void write() {
        x = 5;
        b = true;
        System.out.println("x=>" + x);
        System.out.println("b =>" + b);
    }
    /**
     * 读操作
     */
    private void read() {
        //如果b=false的话,就会无限循环,直到b=true才会执行结束,会打印出x的值
        while (!b) {
        }
        System.out.println("x=" + x);
    }
    public static void main(String[] args) throws Exception {
        final VolatileDemo volatileDemo = new VolatileDemo();
        //线程1执行写操作
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                volatileDemo.write();
            }
        });
        //线程2执行读操作
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                volatileDemo.read();
            }
        });
        //我们让线程2的读操作先执行
        thread2.start();
        //睡1毫秒,为了保证线程2比线程1先执行
        TimeUnit.MILLISECONDS.sleep(1);
        //再让线程1的写操作执行
        thread1.start();
        thread1.join();
        thread2.join();
        //等待线程1和线程2全部结束后,打印执行结束
        System.out.println("执行结束");
    }
}
注意我们的b没有用volatile修饰,我们先启动了线程2的读操作,后启动了线程1的写操作,由于线程1和线程2会保存x和b的副本到自己的工作内存中,线程2执行后,由于他副本b=false,所以会进入到无限循环中,线程1执行后修改的也是自己副本中的b=true,然而线程2无法立即察觉到,所以执行上面代码后,不会打印“执行结束”,因为线程2一直在执行!
运行之后会一直出于运行状态,并且没有打印“执行结束”

 此时的流程会是这样子

给b加了volatile关键字修饰后,线程1对b做了修改,然后会立即更新内存中的值,线程2通过嗅探发现自己的副本已经过期了,然后重新从内存中拿到b=true的值,然后跳出while循环,执行结束!
我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用.
3.原子性验证
看下面一段代码,number变量加了volatile修饰。创建了10个子线程,每个线程循环1000次执行number++。
package com.bruce.demo8;
import java.util.concurrent.TimeUnit;
/**
 * @BelongsProject: SingleTon-2020
 * @BelongsPackage: com.bruce.demo8
 * @CreateTime: 2020-12-10 23:08
 * @Description: TODO
 */
public class Demo8 {
    static class MyTest {
        public volatile int number = 0;
        public void incr(){
            number++;
        }
    }
    public static void main(String[] args) {
        MyTest myTest = new MyTest();
        for (int i = 1; i <= 10; i++){
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++){
                    myTest.incr();
                }
            }, "Thread"+String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //等线程执行结束了,输出number值
        System.out.println("当前number:" + myTest.number);
    }
}
按理说number最终应该是10000,但是这边执行后,结果如下:
4.原子性问题解决
方法一:使用 synchronized 关键字
//给函数增加synchronized修饰,相当于加锁了
 public synchronized void incr(){
     number++;
 }
结果如下:
 方法二:使用AtomicInteger
public class Demo8 {
    static class MyTest {
        public volatile AtomicInteger number = new AtomicInteger();
        
        public void incr(){
            number.getAndIncrement();
        }
    }
    public static void main(String[] args) {
        MyTest myTest = new MyTest();
        for (int i = 1; i <= 10; i++){
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++){
                    myTest.incr();
                }
            }, "Thread"+String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //等线程执行结束了,输出number值
        System.out.println("当前number:" + myTest.number);
    }
}
