十三、JAVA多线程笔记:volatile关键字(编发编程三个重要特性、JMM、volatile )

并发编程的三个重要特性

1.原子性


原子是元素能保持其化学性质的最小单位,在化学反应中不可分割。对应软件开发里面的一个完整的不可拆分的逻辑过程如事务,一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。
java里面看着简单的操作不见得是原子操作:比如自增++ 编译成字节码后它分为三步:ILOAD(加载)-IINC(自增)-ISTORE(存储)。另一方面更复杂的CAS(Compare And Swap)操作却具有原子性。注意即使编译后只有一条字节码也不能保证这个操作一定具有原子性,一条字节码也可能转化成多条机器码。此处字节码已经能够说明问题,所以使用字节码分析。

由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个,我们大致可以认为基本数据类型的访问读写是具备原子性的,如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步快-synchronized关键字,因此synchronized块之间的操作也具备原子性。

2.有序性


有序性即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。(例如:重排的时候某些赋值会被提前)
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性

Java内存模型的有序性是指如果在本线程内观察,所有操作都是有序的;如果一个线程中观察另一个线程,所有的操作都是无序的。前一句是指线程内表现为串行的语义,后一句是指指令重排序现象和工作内存与主内存同步延迟的现象。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

3.可见性


可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
可见性就是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronized和final。同步快的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到初始化了一半的对象),那么在其他线程中就能看见final字段的值。

JMM如何保证三大特性

1.多个原子性性的操作在一起就不再是原子性操作了

2.简单的读取与赋值操作都是原子性的,将一个变量赋给另外一个变量的操作不是原子性的。

3.Java内存模型(JMM)只保证了基本读取和赋值的原子性操作,其他的均不保证,如果想要使得某些代码片段具备原子性,需要使用关键字synchronized,或者JUC中的lock。如果想要使得int等类型自增操作具备原子性,可以使用JUC包下的原子封装类型

java.util.concurrent.atomic.*

volatile关键字不具备保证原子性的语义

 

JMM与可见性

在多线程的环境下,如果某个线程首次读取共享变量,则首先到主内存中获取该变量,然后存入工作内存中,以后只需要在工作内存中读取该变量即可。同样如果对该变量执行了修改的操作,则先将新值写入工作内存中,然后再刷新至主内存中。但是什么时候刷新至主内存中是不大确定的。

volatile关键字具备保证可见性的语义

JMM与有序性

在Java内存模型中,允许编译器和处理器对指令进行重新排序,在单线程的情况下,重排序并不会引起什么问题。

但是多线程情况下,重排序会影响到程序的正确运行。

三种方式:

        1.使用volatile关键字保证有序性

        2.使用synchronized关键字保证有序性。

        3.使用显式锁Lock保证有序性

happens-before

此外,Java的内存模型中具备一些天生的有序性规则,不需要任何同步手段就能保证有序性,这个规则成为happens-beforeguize。

happens-before原则定义如下:

1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 
2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

下面是happens-before原则规则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

volatile关键字的深入解析

voilatile关键字的语义

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。

volatile的使用场景

1.开关控制利用其可见性(线程的关闭)

2.状态标记利用顺序性特点

3.单利模式double-check 利用其顺序性

volatile和synchronized区别

volatile关键字的总结

1.volatile关键字只用修饰变量,不能修饰方法和类。

2.volatile变量的值都是从主存中获取的,而不是从线程的本地内存。

3.long和double变量被volatile关键字修饰之后,读写(赋值操作,读取操作)都是原子操作.

4.使用volatile关键字可以避免内存不一致的错误;写入volatile变量一定会比接下来的读操作先发生。

5.从jdk5开始对volatile变量的修改对其他的线程都是可见的;当线程读取volatile变量的时候,会先把其他线程中缓存着的volatile变量(如果还没有更新到主存中的时候)强制写入到主存。

6.除了long和double其他的基本类型读写操作都是原子性的;引用类型的读写操作也是原子性的。

7.volatile变量只能做简单的读写,没有锁,没有阻塞。

8.volatile变量可以是空的.

9.volatile不能保证原子性,比如volatile修饰的int变量++操作还是非原子的。

10.变量没有在多个线程之间共享,没有必要做任何同步的操作,比如使用volatile关键字修饰。

synchronized和volatile关键字的比较

volatile关键字代替不了synchronized关键字,不过在某些场合可以作为替代方案。

1.volatile关键字只能修饰字段,而synchronized只能修饰代码块和方法。

2.synchronized关键字需要获得锁释放锁,volatile关键字不需要。

3.synchronized代码块或方法在等待锁的时候会被阻塞;volatile不是这样的。

4.synchronized代码块或方法会比volatile关键字更影响性能。

5.volatile关键只同步被修饰的变量,而synchronized关键字却同步代码块或方法中所有的变量,并且还会获得锁释放锁,所以synchronized的负载更大。

6.不能同步(synchronized)null对象,而volatile变量可以是null的。

7.读取volatile变量效果等同获取锁,写入volatile变量效果等同释放锁。

猜你喜欢

转载自blog.csdn.net/zhanglong_4444/article/details/86222072
今日推荐