Java中的volatile关键字最全总结(三)

pivoteic
发布于 2022-6-15 17:05
浏览
0收藏

 

happens-before原则

由于有指令重排序的存在,这将导致我们在分析多线程程序的时候出现一些难以预料的问题,这些问题往往又很难被发现。基于此,从JDK1.5开始,官方提出了happens-bofore原则,指的是如果一个线程对某个变量的操作在另一个线程对该变量的操作之前,则第一个线程的操作必须对第二个线程可见。happens-before共有六项规则:

1.程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作;它表示在一个线程中的每个操作,对于其后续的操作都应该是可见的

2.监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁;它表示某个线程在解锁之前的所有操作,都应该对另一个加锁的线程可见

3.volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读;它表示某个线程在写入一个volatile修饰的变量之前的所有操作都应该对读取这个volatile修饰的变量的线程可见

4.传递性:如果A happens-before B,且B happens-before C,那么A happens-before C;它表示线程A对线程B可见,线程B对线程C可见,则线程A就对线程C可见

5.start()规则:如果线程A执行操作 ThreadB.start() ,那么线程A的 ThreadB.start() 操作happens-before于线程B的任意操作;它表示线程A在启动线程B时,此时所有对共享变量的操作都对线程B可见,但线程B启动之后,线程A再对共享变量进行操作,线程B无法保证可见

6.join()规则:如果线程A执行操作 ThreadB.join() 并成功返回,那么线程B中的任意操作happens-before于线程A从 ThreadB.join() 操作成功返回;它表示线程B在调用 ThreadA.join() 并成功返回后,线程A所有对共享变量的操作都对线程B可见

 

由此我们可知,对于刚才的案例,如果对四个变量添加了volatile关键字,则线程在对其进行操作时,互相都是可见的,比如线程t1将a赋值为1的时候,线程t2读取了变量a,那么线程t1在为变量a赋值及其之前的所有操作都将对线程t2可见,所以当线程t1将变量a的值修改为1时,线程t2读取到的变量i值一定为0,变量j值一定为1。

 

基于happens-before原则,volatile重排序也受到了相应的限制:

•当对volatile变量进行写操作时,无论前一个操作是什么,都不能重排序

•当对volatile变量进行读操作时,无论后一个操作是什么,都不能重排序

•当先对volatile变量进行写操作,后进行读操作时,不能重排序

 

volatile在单例模式中的应用

来温习一下单例模式的书写:

publicclassSingletonDemo{privatestaticfinalSingletonDemosingletonDemo=newSingletonDemo();privateSingletonDemo(){}publicSingletonDemogetInstance(){returnsingletonDemo;}}


这是饿汉式单例的一种写法,还有懒汉式单例的实现:

publicclassSingletonDemo{privatestaticSingletonDemosingletonDemo;privateSingletonDemo(){}publicSingletonDemogetInstance(){if(singletonDemo==null){synchronized(SingletonDemo.class){if(singletonDemo==null){singletonDemo=newSingletonDemo();}}}returnsingletonDemo;}}


这种方式通常被认为是高效的、线程安全的,然而这种方式仍然面临着一个问题,需要知道的是,对象的创建分为以下几个步骤:

 

1.JVM首先对new的内容进行解析,在常量池中查找一个类的符号引用

2.若是没有找到符号引用,则认为该类是没有被加载的,所以JVM会对其进行类的加载、解析和初始化

3.JVM为对象分配内存

4.将分配的内存初始化为零值

5.调用对象的方法

 

这些步骤可以简化为:分配内存,初始化实例,返回引用。现在假设线程A执行到了 singletonDemo = new SingletonDemo() ,但由于创建对象的过程并不是一个原子性的操作,且编译器可能会对创建对象的操作进行重排序,所以当JVM为对象分配了内存之后,很有可能会将返回引用的操作提前,此时该引用还没有进行初始化等操作,接着线程B抢占到了执行权,其判断singletonDemo不为空,就能够直接获取到singletonDemo的引用,但它仅仅是一个半成品,还没有进行接下来初始化的操作,此时线程B使用着这个半成品就会出现一些无法预料的问题。

正确的办法是使用volatile修饰单例变量:

private static volatile SingletonDemo singletonDemo;


这样就能避免指令的重排序,使对象的创建步骤正常有序地进行。

 

文章转自公众号:三友的java日记

标签
已于2022-6-15 17:05:59修改
收藏
回复
举报
回复
    相关推荐