Volatile与可见性

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

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

运行,问题完美解决。

Volatile的作用:

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

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

正是由于这两个原因,所以Volatile可以保证“可见性”

volatile变量为什么在并发下不安全?

volatile变量在各个线程的工作内存中也可以存在不一致的情况,但由于每次使用之前都要刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题,但是Java里面的运算并非原子操作。

假如说一个写入值操作不需要依赖依赖这个值的原先值,那么在进行写入的时候我们就不需要进行读取操作。写入操作对原本的值的时候没有要求,那么所有线程都可以写入新的值,虽然读取到的值是相同的,每个线程的操作也是正确的,但是最终结果却是错误的。

图片 1JMM

感兴趣的可以运行如下代码:

public class VolatileTest { public static volatile int count = 0; public static final int THREAD_COUNT = 20; public static void add(){ count++; } public static void main(String[] args) { Thread[] threads = new Thread[THREAD_COUNT]; for (int i = 0; i < THREAD_COUNT; i++) { threads[i] = new Thread -> { for (int j = 0; j < 1000; j++) { add; threads[i].start(); } for (int i = 0; i < THREAD_COUNT; i++) { threads[i].join(); } System.out.println; } }// 如果并发正确的话:应该是20000,但是每次运行结果都不到20000

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可以保证“有序性”,“可见性”,但是无法保证“原子性”

图片 2与文无关

一个小栗子

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

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

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

这是为什么?这又和内存模型有关了,由此可见,内存模型是多么重要,不光是Synchronized,还是这次的Volatile都和内存模型有关。

Synchronized

Synchronized保证了原子性,可见性与有序性,它的工作时对同步的代码块加锁,使得每次只有一个线程进入代码块,从而保证线程安全。synchronized反应到字节码层面就是monitorenter与monitorexit.

注意*:虽然synchonized关键字看起来是万能的,能保证线程安全性,但是越万能的控制往往越伴随着越大的性能影响。

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

Synchonzied案例
public class SynchronziedTest implements Runnable{ static int i = 0; static int j = 0; static SynchronziedTest instance= new SynchronziedTest(); @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } } public synchronized void increase(){ i++; } public static void main(String[] args) throws InterruptedException { // 注意新建的线程指向的同一个实例, // 如果指向不同的实例,那么两个线程关注的锁就不是同一把锁,就会导致线程不安全 Thread t1 = new Thread; Thread t2 = new Thread; //错误的用法// Thread t3 = new Thread(new SynchronziedTest;// Thread t4 = new Thread(new SynchronziedTest; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println; }}//结果为:200000

注意创建线程的时候指向同一个实例,才会锁住相同的对象。

题外话

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

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

public class Main {    private static boolean isStop = false;    public static void main(String[] args) {        new Thread -> {            while  {                if  {                    System.out.println("结束");                    return;                }            }        }).start();        try {            TimeUnit.SECONDS.sleep;            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  {                try {                    TimeUnit.SECONDS.sleep;                } catch (InterruptedException e) {                    e.printStackTrace();                }                if  {                    System.out.println("结束");                    return;                }            }        }).start();        try {            TimeUnit.SECONDS.sleep;            isStop = true;        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

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

public class Main {    private static boolean isStop = false;    public static void main(String[] args) {        new Thread -> {            while  {                System.out.println("Hello");                if  {                    System.out.println("结束");                    return;                }            }        }).start();        try {            TimeUnit.SECONDS.sleep;            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  {                Random random=new Random();                random.nextInt;                if  {                    System.out.println("结束");                    return;                }            }        }).start();        try {            TimeUnit.SECONDS.sleep;            isStop = true;        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

这样也可以。

为什么呢?

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

Synchonzied用法
  1. 实例方法上,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  2. 静态方法上,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  3. 实例方法代码块.
  4. 静态方法代码块。

 //实例方法 public synchronized void add(int value){ this.count += value; } //静态方法 public static synchronized void add(int value){ count += value; } //实例方法代码块 public void add(int value){ synchronized{ this.count += value; } } //静态方法代码块 public class MyClass { public static synchronized void log1(String msg1, String msg2){ log.writeln; log.writeln; } public static void log2(String msg1, String msg2){ synchronized(MyClass.class){ log.writeln; log.writeln; } } } 

Volatile与有序性

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

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

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

所以,Volatile可以保证“有序性”。

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

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

图片 3

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

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

Volatile适合做什么?

适合做标量,当一个线程对某个变量进行读写操作,而其它线程仅仅进行读操作的时候,是可以保证volatile的正确性的。如下:

volatile bool stopped;public void stop(){ stopped = true}while(!stoppped){ // 执行操作}

发表评论

电子邮件地址不会被公开。 必填项已用*标注