volatile

volatile是什么

volatile 是一个类型修饰符,使用方式以下
private volatile int a = 0;java

线程安全的前提

  • 原子性
    一个或者多个操做,要么所有执行而且中途不能被打断,要么都不执行。安全

  • 可见性
    同一个线程里,先执行的代码结果对后执行的代码可见,不一样线程里任意线程对某个变量修改后,其它线程可以及时知道修改后的结果。多线程

  • 有序性
    同一线程里,程序的执行顺序按照代码的前后顺序执行。并发

只有知足了以上三个前提,才能说线程是安全的性能

volatile的做用

volatile关键字在多线程中,只保证可见性、有序性。但不保证原子性。测试

1. 保证可见性

来看一个网上找的例子线程

public class TestVolatele {
	//测试一
    private static boolean isOk = true;
	//测试二
	//private static volatile boolean isOk = true;

	public static void main(String[] args) throws InterruptedException {
		new Thread(() -> {
			System.out.println(Thread.currentThread().getName() + "--开始循环了");
			while (isOk) {
			}
			System.out.println(Thread.currentThread().getName() + "--跳出循环了");
		}, "t1").start();

		new Thread(() -> {
			try {
				Thread.sleep(2000);
			} catch (Exception e) {
			}
			isOk = false;
			System.out.println(Thread.currentThread().getName() + "--isOk改成 false");
		}, "t2").start();
	}
}

这个例子很简单,t1线程会一直监听isOk字段,t2线程负责修改isOk字段,正常状况下,当t2把isOk改成false时,t1应该会退出while循环,运行代码来验证一下结果。code

上图是没有加volatile关键字的运行结果。能够看到线程 t1 并无执行完。
这说明线程 t2 修改了 isOk = false 以后,在线程 t1 中并不知道该字段被修改了。blog

上图加volatile关键字的运行结果。线程t1也执行完了。
能够看出加了volatile关键字,那么isOk就具有了可见性。排序

为了解释可见性的缘由,能够看上图的java内存模型图。isOk = true这个字段其实时存在主内存中的。当线程要操做isOk时,先把isOk复制一份到本身的工做内存中,在工做内存中对字段操做完后,会再把字段写入主内存。

假设没有volatile字段时
一、t1 线程从主内存复制 isOk = true 到 t1 工做内存
二、t2 线程从主内存复制 isOk = true 到 t2 工做内存
三、t2 修改工做内存 isOk = false,并赋值给主内存(主内存isOk = false)
四、t1 线程读取的仍是 t1 工做内存(isOk= true),并不知道主内存isOk已改成false
五、因为没法实时获取主内存最新数据,因此致使一直while循环

有volatile字段时
一、t1 线程从主内存复制 isOk = true 到 t1 工做内存
二、t2 线程从主内存复制 isOk = true 到 t2 工做内存
三、t2 修改工做内存 isOk = false,并赋值给主内存(主内存isOk = false)
四、isOk 因为加了volatile关键字,这时 t1 线程强制读取主内存数据
五、读取到主内存isOk=false,退出while循环(能够理解为volatile关键字对主内存保证可见性)

2.保证有序性(禁止指令重排序)

什么是有序性?咱们写的Java程序代码不老是按顺序执行的,都有可能出现程序重排序(指令重排)的状况,这么作的好处就是为了让执行块的程序代码先执行,执行慢的程序放到后面去,提升总体运行效率。

int a = 1;
int b = 2;

上述的两条赋值语句在程序运行时,并不必定按照顺序先给a赋值,而后再给b赋值,颇有可能先执行b再执行a,这是程序为了提升效率出现了指令重排序。虽然在单个线程中,指令重排序不会对结果产生任何问题,可是在多线程中出现指令重排序,可能会致使最终的结果不是咱们想要的。

举例个单例模式(懒汉式)的例子

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

若是上边的代码不使用volatile关键字,可能会出现问题。问题在于 instance = new Singleton();这行代码,其实这行代码能够拆分为

一、为instance分配内存
二、初始化instance
三、将instance变量指向分配的内存空间

现有A和B两条线程同时调用 getInstance() 方法,假设A线程先执行了instance = new Singleton() 而且发生了指令重排序。可能会出现A线程先执行第三步,后执行第二步的状况。也就是说可能会出现instance变量还没初始化完成,B线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的状况。因此在单例的懒汉式中须要加上volatile关键字禁止指令重排序

3.不保证原子性

public class TestVolatele {

    private static volatile long n = 0;

	public static void main(String[] args) throws Exception {
		List<Thread> tList = new ArrayList<>();
		for (int i = 0; i < 5; i++) {
			tList.add(new Thread(() -> {
				for (int j = 0; j < 2000; j++) {
					n++;
				}
			}));
		}
		for (Thread thread : tList) {
			thread.start();
		}
		for (Thread thread : tList) {
			thread.join();
		}
		System.out.println(n);
	}
}

上面的代码开启5条线程,每条线程对n++两千次。若是volatile关键字具有原子性,那么结果确定等于10000。但实际上每次运行的结果都不一样,结果中n老是 <= 10000,这说明volatile不具有原子性。

但可能会疑惑,volatile关键字不是直读取主内存吗?明明能够实时拿到到主内存的最新数据,为何还不保证原子性?这就须要把n++给拆分来解释

能够把n++拆分为3个阶段
一、读取 n
二、对 n 加 1
三、把 n 写入主内存

把n++拆分以后,再来分析结果 n <= 10000 的缘由

假设A、B两条线程操做 n++,而且n初始值为0
一、A 加载主内存 n=0 到 A 工做内存
二、B 加载主内存 n=0 到 B 工做内存
三、A 在工做内存中执行 n++ ,把结果 n=1 写入主内存
四、B 强制读取主内存 n = 1 ,并执行 n++ 操做(到这里都没问题)
五、可是,B 在执行 n++ 时,只执行了 n++ 的第1步,读取 n=1(这时 B 就中止了,CPU切换到A线程执行)
六、此时线程 A 执行了 n++ ,而且执行完了,把结果写入主内存 n=2
七、CPU又切换到 B 执行了,B 执行 n++ 的第2步,对n加1。(此时 B 中 n=2)
八、B 执行完后,把 n=2 写入主内存,这就致使了主内存中写入了两次 n=2

上边的举例因为主内存写入两次 n=2,因此最终致使 n <= 10000 的,因此说 volatile 不保证原子性。 volatile 它只针对读取时可见,既读取时的数据保证最新的,可是并不保证写入数据时不存在问题。

volatile应用场景

volatile的应用要从它的特性入手,只保证可见性、有序性。但不保证原子性。

(1)volatile最适合使用的地方是一个线程写、其它线程读的场合,若是有多个线程并发写操做,仍然须要使用锁或者线程安全的容器或者原子变量来代替。 (2)假如一个线程写、一个线程读,根据前面针对volatile的应用总结,此时可使用volatile来代替传统的synchronized关键字提高并发访问的性能。 (3)volatile不适合多个线程同时写的状况,由于volatile不保证原子性,多线程同时写会有问题

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息