volatile关键字

无人久伴 提交于 2019-12-28 16:27:54

volitale关键字

简介:
  volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或方法)和volatile变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

修饰的变量只能做赋值操作,不能做自增操作

1. 并发编程的3个基本概念

(1) 原子性

定义:即一个操作或者多个操作,要么全部执行并且执行过程不会被任何因素打断,要么就都不执行。

(2) 可见性

定义: 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

(3) 有序性

定义: 即线程执行的顺序按照代码的先后顺序执行

2. volatile关键字作用
  1. 保证内存可见性
    private static volatile boolean flag = true;
    public static main(String[] args){
        new Thread(()->{
            try{
                Thread.sleep(5000);
            } catch (InterruptedException){
                e.printStackTrace();
            }
            flag = false;
        }).start();
        while(flag){
            system.out.println("............")
        }
    }

    注意:
        在上述代码中,如果不加关键字volatile,while循环将一直执行无法跳出循环。
        因为Java的内存机制,每一个内存都有自己的一个独有的工作内存,
        如果工作内存的数据没有发生改变,此线程就不会再去读取主内存
        或者将修改过后的值放回到主内存中。所以就算其他线程修改了flag的值,
        while也不会去读取主内存中修改过的flag值,就会一直使用
        工作内存的flag值,所以会一直循环下去。
  1. 禁止指令重排

  指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM可能会对指令进行重排序。最金典的例子就是多线程场景下的双重检查加锁的单例实现:

public class Singleton {
 
    private volatile static Singleton instance;
 
    private Singleton() {}
 
    public static Singleton getInstance() {     //1
        if (instance == null) {                 //2
            synchronized (Singleton.class) {    //3
                if (instance == null) {         //4
                    instance = new Singleton(); //5
                }
            }
        }
        return instance;
    }
}

注意:
    需要加volatile关键字的原因是,在并发情况下,如果没有这个关键字,
    在第5行会出现问题。因为第五行代码“instance = new Singleton()”
    并不是原子性操作,在JVM中被分为如下三个阶段执行:
        1. 为instance分配内存
        2. 初始化instance
        3. 将instance变量指向分配的内存空间
        
            由于JVM可能存在重排序,可能会执行第3步然后再执行第2步。也就是说可能
        会出现instance变量还没有初始化完成,其他线程就已经判断了该变量不为null,
        结果返回了 一个没有初始化的半成品。而加上volatile关键字,可以保证instance
        变量的操作不会被JVM重排。
3. 不保证原子性

  虽然volatile关键字可以保证内存可见性和有序性,但是不能保证原子性。也就是说对volatile修饰的变量进行的操作,不保证多线程安全。如下实例:

    private static CountDownLatch countDownLatch = new CountDownLatch(1000);
    private volatile static int num = 0;
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                try {
                    num++;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();
        System.out.println(num);
    }

  静态变量num被volatile所修饰,并且同时开启1000个线程对其进行累加的操作,按道理来说,其结果应该为1000,但实际的情况是,每次运行结果都是一个小于1000的数字,并且不固定。那么这是为什么呢?原因是因为“num++;”这行代码并不是原子操作,尽管它被volatile所修饰了也依然如此。++操作的执行过程如下面所示:

  1. 首先获取变量i的值
  2. 将该变量的值+1
  3. 将该变量的值写回到对应的主内存中

  虽然每次获取num值的时候,也就是执行上述第一步的时候,都拿到的是主内存的最新变量值,但是在进行第二步num+1的时候,可能其他线程在此期间已经对num做了多次修改,这时再进行第二三步操作之后就会覆盖了一个旧值,发生了错误。比如说:线程A在执行第一步的时候读取到此时num的值为3,然后在执行第二步之前,其他多个线程已经对该值进行了多次修改,使得num值变为了10。而线程A此时执行第二步,将原先的num值为3的结果+1变为了4,最后再将4写回到主内存中(实际此时num应该为11)。所以这也就是最后的执行结果为什么都会是一个小于1000的值的原因,内存可见性只能保证在第一步操作上的内存可见性而已。

  所以如果要解决上面代码的多线程安全问题,可以采取加锁synchronized的方式,也可以使用concurrent包下的原子类AtomicInteger,以下的代码演示了使用AtomicInteger来包装num变量的方式:

    private static CountDownLatch countDownLatch = new CountDownLatch(1000);
    private static AtomicInteger num = new AtomicInteger();
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                try {
                    num.getAndIncrement();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();
        System.out.println(num);
    }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!