当前位置: 移动技术网 > IT编程>开发语言>Java > Volatile的那些事

Volatile的那些事

2018年12月27日  | 移动技术网IT编程  | 我要评论
上一篇中,我们了解了Synchronized关键字,知道了它的基本使用方法,它的同步特性,知道了它与Java内存模型的关系,也明白了 Synchronized可以保证“原子性”,“可见性”,“有序性” 。今天我们来看看另外一个关键字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秒钟后,会打印出一句话,并且结束循环。但是,出人意料的事情发生了,等了很久,这句话迟迟没有出现,也没有结束循环。

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

问题分析

我们再来看看内存模型:

image.png

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

如果把这个搞明白了,就很容易理解为什么会产生上面的情况了:

isstop是共享数据,放在了主内存,子线程需要这个数据,就把数据复制到自己的本地内存,此时isstop=false,以后直接读取本地内存就可以。主线程修改了isstop,子线程是无感知的,还是去本地内存中取数据,得到的isstop还是false,所以就造成了上面的情况。

volatile与可见性

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

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是强制内存“可见性”,而这里是可能可以。

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网