Volatile的那些事

上一篇中,咱们了解了Synchronized关键字,知道了它的基本使用方法,它的同步特性,知道了它与Java内存模型的关系,也明白了Synchronized能够保证“原子性”,“可见性”,“有序性”。今天咱们来看看另一个关键字Volatile,这也是极其重要的关键字之一。绝不夸张的说,面试的时候谈到Synchronized,一定会谈到Volatile。面试

一个小栗子

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (isStop) {
                    System.out.println("结束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

首先定义了一个全局变量:isStop=false。而后在main方法里面开了一个线程,里面是一个死循环,当isStop=true,打印出一句话,结束循环。主线程睡了三秒钟,把isStop改成true。安全

按道理来讲,3秒钟后,会打印出一句话,而且结束循环。可是,出人意料的事情发生了,等了好久,这句话迟迟没有出现,也没有结束循环。bash

这是为何?这又和内存模型有关了,因而可知,内存模型是多么重要,不光是Synchronized,仍是此次的Volatile都和内存模型有关。多线程

问题分析

咱们再来看看内存模型:app

image.png

线程的共享数据是存放在主内存的,每一个线程都有本身的本地内存,本地内存是线程独享的。当一个线程须要共享数据,是先去本地内存中查找,若是找到的话,就不会再去主内存中找了,须要修改共享数据的话,是先把主内存的共享数据复制一份到本地内存,而后在本地内存中修改,再把数据复制到主内存。dom

若是把这个搞明白了,就很容易理解为何会产生上面的状况了:性能

isStop是共享数据,放在了主内存,子线程须要这个数据,就把数据复制到本身的本地内存,此时isStop=false,之后直接读取本地内存就能够。主线程修改了isStop,子线程是无感知的,仍是去本地内存中取数据,获得的isStop仍是false,因此就形成了上面的状况。ui

Volatile与可见性

如何解决这个问题呢,只须要给isStop加一个Volatile关键字:spa

public class Main {
    private static volatile boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (isStop) {
                    System.out.println("结束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

运行,问题完美解决。线程

Volatile的做用:

  1. 当一个变量加了volatile关键字后,线程修改这个变量后,强制当即刷新回主内存。

  2. 若是其余线程的本地内存中有这个变量的副本,会强制把这个变量过时,下次就不能读取这个副本了,那么就只能去主内存取,拿到的数据就是最新的。

正是因为这两个缘由,因此Volatile能够保证“可见性”

Volatile与有序性

指令重排的基本概念就再也不阐述了,上两节内容已经介绍了指令重排的基本概念。

指令重排遵照的happens-before规则,其中有一条规则,就是Volatile规则:

被Volatile标记的不容许指令重排。

因此,Volatile能够保证“有序性”。

那内部是如何禁止指令重排的呢?在指令中插入内存屏障

内存屏障有四种类型,以下所示:

image.png

在生成指令序列的时候,会根据具体状况插入不一样的内存屏障。

总结下,Volatile能够保证“可见性”,“有序性”

Volatile与单例模式

public class Main {
    private static Main main;

    private Main() {
    }

    public static Main getInstance() {
        if (main == null) {
            synchronized (Main.class) {
                if (main == null) {
                    main = new Main();
                }
            }
        }
        return main;
    }
}
复制代码

这里比较经典的单例模式,看上去没什么问题,线程安全,性能也不错,又是懒加载,这个单例模式还有一个响当当的名字:DCL

可是实际上,仍是有点问题的,问题就出在

main = new Main();
复制代码

这又和内存模型有关系了。执行这个建立对象会有3个步骤:

  1. 分配内存
  2. 执行构造方法
  3. 指向地址

说明建立对象不是原子性操做,可是真正引发问题的是指令重排。先执行2,仍是先执行3,在单线程中是无所谓的,可是在多线程中就不同了。若是线程A先执行3,还没来得及执行2,此时,有一个线程B进来了,发现main不为空了,直接返回main,而后使用返回出来的main,可是此时main还不是完整的,由于线程A尚未来得及执行构造方法。

因此单例模式得在定义变量的时候,加上Volatile,即:

public class Main {
    private volatile static Main main;

    private Main() {
    }

    public static Main getInstance() {
        if (main == null) {
            synchronized (Main.class) {
                if (main == null) {
                    main = new Main();
                }
            }
        }
        return main;
    }
}
复制代码

这样就能够避免上面所述的问题了。

好了,这篇文章到这里主要内容就结束了,总结全文:Volatile能够保证“有序性”,“可见性”,可是没法保证“原子性”

题外话

嘿嘿,既然上面说的是主要内容结束了,就表明还有其余内容。

咱们把文章开头的例子再次拿出来:

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (isStop) {
                    System.out.println("结束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

若是既想让子线程结束,又不想加Volatile关键字怎么办?这真的能够作到吗?固然能够。

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (isStop) {
                    System.out.println("结束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

在这里,我让子线程也睡了一秒,运行程序,发现子线程中止了。

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                System.out.println("Hello");
                if (isStop) {
                    System.out.println("结束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

我把上面的让子线程睡一秒钟的代码替换成 System.out.println,居然也成功让子线程中止了。

public class Main {
    private static boolean isStop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                Random random=new Random();
                random.nextInt(150);
                if (isStop) {
                    System.out.println("结束");
                    return;
                }
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
            isStop = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

这样也能够。

为何呢?

由于JVM会尽力保证内存的可见性,即便这个变量没有加入Volatile关键字,主要CPU有时间,都会尽力保证拿到最新的数据。可是第一个例子中,CPU不停的在作着死循环,死循环内部就是判断isStop,没有时间去作其余的事情,可是只要给它一点机会,就像上面的 睡一秒钟,打印出一句话,生成一个随机数,这些操做都是比较耗时的,CPU就可能能够去拿到最新的数据了。不过和Volatile不一样的是 Volatile是强制内存“可见性”,而这里是可能能够。

相关文章
相关标签/搜索