volatile

volatile是什么

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

线程安全的前提

  • 原子性
    一个或者多个操作,要么全部执行并且中途不能被打断,要么都不执行。

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

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

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

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循环,运行代码来验证一下结果。

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

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

为了解释可见性的原因,可以看上图的java内存模型图。isOk = true这个字段其实时存在主内存中的。当线程要操作isOk时,先把isOk复制一份到自己的工作内存中,在工作内存中对字段操作完后,会再把字段写入主内存。

假设没有volatile字段时
1、t1 线程从主内存复制 isOk = true 到 t1 工作内存
2、t2 线程从主内存复制 isOk = true 到 t2 工作内存
3、t2 修改工作内存 isOk = false,并赋值给主内存(主内存isOk = false)
4、t1 线程读取的还是 t1 工作内存(isOk= true),并不知道主内存isOk已改为false
5、由于无法实时获取主内存最新数据,所以导致一直while循环

有volatile字段时
1、t1 线程从主内存复制 isOk = true 到 t1 工作内存
2、t2 线程从主内存复制 isOk = true 到 t2 工作内存
3、t2 修改工作内存 isOk = false,并赋值给主内存(主内存isOk = false)
4、isOk 由于加了volatile关键字,这时 t1 线程强制读取主内存数据
5、读取到主内存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();这行代码,其实这行代码可以拆分为

1、为instance分配内存
2、初始化instance
3、将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个阶段
1、读取 n
2、对 n 加 1
3、把 n 写入主内存

把n++拆分之后,再来分析结果 n <= 10000 的原因

假设A、B两条线程操作 n++,并且n初始值为0
1、A 加载主内存 n=0 到 A 工作内存
2、B 加载主内存 n=0 到 B 工作内存
3、A 在工作内存中执行 n++ ,把结果 n=1 写入主内存
4、B 强制读取主内存 n = 1 ,并执行 n++ 操作(到这里都没问题)
5、但是,B 在执行 n++ 时,只执行了 n++ 的第1步,读取 n=1(这时 B 就停止了,CPU切换到A线程执行)
6、此时线程 A 执行了 n++ ,并且执行完了,把结果写入主内存 n=2
7、CPU又切换到 B 执行了,B 执行 n++ 的第2步,对n加1。(此时 B 中 n=2)
8、B 执行完后,把 n=2 写入主内存,这就导致了主内存中写入了两次 n=2

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

volatile应用场景

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

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

posted @ 2021-08-06 14:27  、嘎路的米。  阅读(83)  评论(0)    收藏  举报