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的作用:
当一个变量加了volatile关键字后,线程修改这个变量后,强制立即刷新会主内存。
假如其余线程的本地内存中有这个变量的副本,会强制把这个变量过期,下次就不能读取这个副本了,那么就只能去主内存取,拿到的数据就是最新的。
正是因为这两个起因,所以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个步骤:
- 分配内存
- 执行构造方法
- 指向地址
说明创立对象不是原子性操作,但是真正引起问题的是指令重排。先执行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是强制内存“可见性”,而这里是可能可以。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Volatile的那些事