Volatile关键字

Volatile 是 Java 虚拟机提供 轻量级的同步机制(可理解为弱化版的synchronized)

作用

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

保证可见性

示例

import java.util.concurrent.TimeUnit;

public class TestVolatile {
    //加了volatile 是可以保证可见性的
    private volatile static Integer number = 0;

    public static void main(String[] args) {
        new Thread(()->{
            while (number==0){
               //此处不能写有锁的方法
                //(比如输出方法中含有synchronized,否则演示会失败)
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        number = 1;
        System.out.println("主线程更改number后"+number);
    }
}

属性number不加关键字Volatile ,经过运行我们发现程序不会停止进入了死循环。这是因为主线程对number进行修改后,在其它线程中对number属性的修改不可见,所以循环条件一直成立。解决办法就是给要改变的属性加上volatile关键字,保证属性被修改后的可见性

不保证原子性

原子性:不可分割。某个线程在执行任务的时候,不能被其它线程打扰的,也不能被分割的。

volatile 是不保证原子性的。

示例

public class TestVolatile {
    private static volatile int number = 0;

    public static void add() {
        //++ 不是一个原子操作,是2~3个操作
        number++;
    }

    public static void main(String[] args) {
        //下面线程执行完理论上 number === 20000
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            // main gc
            Thread.yield();
        }
        //最终执行完的结果
        System.out.println(number);
    }
}

上面的例子运行结果不等于20000,并且每次的结果都不一样。这是因为volatile 是不保证原子性的。

问题:除了使用lock锁和synchronized锁外,还能使用什么方法保证原子性?

上面的示例中num++不是原子性操作。通过查看底层源码我们就能知道。

num++底层源码不是原子性操作

答:可以使用原子类进行操作,这样就可以保证原子性。

一些原子类

将上面的示例改为原子类操作就能保证原子性了。下面的例子结果为20000,是我们想要的结果。

import java.util.concurrent.atomic.AtomicInteger;

public class TestVolatile {
    //private static volatile int number = 0;
    private static volatile AtomicInteger number = new AtomicInteger();

    public static void add() {
        //++ 不是一个原子操作,是2~3个操作
        //number++;
        number.incrementAndGet();//底层是 CAS 保证原子性
    }

    public static void main(String[] args) {
        //下面线程执行完理论上 number === 20000
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            // main gc
            Thread.yield();
        }
        //最终执行完的结果
        System.out.println(number);
    }
}

使用原子类的效率比使用锁的效率高很多。

禁止指令重排

我们写的程序,计算机并不是按照我们自己写的那样去执行的。在执行过程中指令的顺序可能会发生改变。这就是指令重排。

处理器在进行指令重排的时候,会考虑数据之间的依赖性!,并不会盲目的指令重排,造成结果错误。

int x=1; //1
int y=2; //2
x=x+5;   //3
y=x*x;   //4

//我们期望的执行顺序是 1234  
//可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的(这样排会造成结果错误)

示例:如果在下面例子中不禁止指令重排可能造成的影响结果如下:

前提:a b x y这四个值 默认都是0

线程 A 线程B
x = a y = b
b = 1 a = 2

正常的结果: x = 0; y =0;

线程A 线程B
b=1 a=2
x=a y=b

如果不禁止指令重排可能在线程A中,先执行b=1,然后再执行x=a

在线程B中可能会出现,先执行a=2,然后执行y=b;

那么结果就可能是:x=2; y=1。

Volatile 如何用

  1. Volatile的一篇文章推荐
  2. 单例模式中的DCL懒汉式中就用到了Volatile

参考教程: https://www.kuangstudy.com/

posted @ 2021-04-30 11:26  懒鑫人  阅读(47)  评论(0编辑  收藏  举报