大纲
1.并发编程的三大特性
2.JMM如何保证并发编程的三大特性
3.volatile关键字理解
4.volatile使用场景
5.volatile和synchronized
1.并发编程的三大特性
原子性:指在一次或多次操作中,要么所有的操作全部正常执行完成(没有异常中断),要么都不执行。
可见性:指当一个线程对共享变量进行了修改,另外的线程可以立即看到修改之后的结果。
有序性:指程序代码在执行过程中的先后顺序,由于Java在编译器以及运行期的优化,导致代码的执行顺序未必就是开发者编写的代码的顺序,但是它会保证程序的最终运算结果是编码时所期望的那样。
有序性举例1:x++与y=20不管他们的执行顺序是怎样的,最后的执行结果肯定的都是x=11,y=20;
int x = 10; int y = 0; x++; y = 20;
有序性举例2:绝对不会出现y=x+1优先于x++执行的情况。
int x = 10; int y = 0; x++; y = x++;
在单线程的情况下,无论怎样的指令重排最终都会保证执行结果与代码顺序执行的结果一致,但是多线程情况下则无法保证有序性。
//initialized用于判断context是否已经被加载过 private boolean initialized = false; private Context context; public Context load(){ if(!initialized){ context = loadContext();//#1 initialized = true; //#2 } return context; }
在多线程情况下发生了指令重排,#2的执行被重排序到#1前面时,第一个线程进来判断 initialized = false 执行context的加载,然后先执行#2将initialized = true,此时第二个线程进来判断initialized = true直接返回还未被加载成功的context,就会导致后续的处理出现问题。可以将initialized变量用volatile关键字修饰禁止指令重排来解决(private volatile boolean initialized = false;)
2.JMM如何保证并发编程的三大特性
JVM采用内存模型的机制来屏蔽各个平台与操作系统之间的内存访问差异,以实现让Java程序在各种平台下达到一致的内存访问效果。如C语言中的整型变量在某些平台占用2个字节,在另外的平台可能占用4个字节,但是Java在任何平台都是占用4个字节。
JMM与原子性:Java语言对基本数据类型的变量、引用类型的变量的读取和赋值操作都是原子性的。
案例1:x=1原子性操作,包含两个步骤:
案例2:y=x或y++或z=z+1都非原子性操作,包含三个步骤:
案例2比案例1都多了一步将数据从主存读取到CPU Cache的操作。由此我们得出以下结论:
volatile关键字不具备保证原子性的语义。
JMM与可见性:在多线程环境下,如果某个线程首次读取共享变量,则首先到主存中获取该变量,然后存入到自己的工作内存中,后续只需要操作工作内存中的变量即可。如果对该变量进行了修改,则先将新值写入工作内存中,然后再刷新到主内存中。但是什么时候刷新到主内存是不确定的。Java提供如下三种方式来保证:
volatile关键字具备保证可见性的语义。
JMM与有序性:多线程情况下,指令重排会影响到程序的正确性,Java提供了三种保证有序性的方式:
Java内存模型具备天生的有序性规则,不需要任何同步手段就能够保证有序性,这个规则称为Happens-before原则,具体有如下几种:
volatile关键字具备保证顺序性的语义。
3.volatile关键字理解
volatile关键字只能修饰类变量和实例变量,不能修饰类常量、实例常量、方法参数、局部变量。被volatile关键字修饰的类变量和实例变量具有如下两种语义:
volatile关键字保证可见性;
volatile关键字保证顺序性;
volatile关键字不保证原子性,比如假设i=10,现在执行i++自增操作:
package com.sFace.test; public class HelloWorld { private volatile static int i = 10; public static void main(String[] args) { i++; } }
对应字节码文件如下:
public class com.sFace.test.HelloWorld { public com.sFace.test.HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field i:I 3: iconst_1 4: iadd 5: putstatic #2 // Field i:I 8: return static {}; Code: 0: bipush 10 2: putstatic #2 // Field i:I 5: return }
0: getstatic指令将变量从主存中取出来,如果此变量是volatile修饰的,则可以保证此时取到的是最新值。
3: iconst_1指令(将常量压入栈中)和4: iadd指令执行过程中,由于CPU时间片调度的关系,执行权切换到其他线程,其他线程对i值进行了修改但是该线程并不会重新去读取最新值。
4.volatile关键字使用场景
不要将volatile用在getAndOperate场合,仅仅set或者get的场景是适合volatile的。
使用场景1:开关控制(可见性)
package com.sFace.test; public class SwitchThread extends Thread{ /** * 开关 */ private static volatile boolean isRunning = true; @Override public void run() { while(isRunning){ System.out.println("I am Running"); } } /** * 停止工作 */ public void shutDown(){ isRunning = false; } }
当某个线程执行了shutDown()方法时,所有的线程会立刻看到isRunning发生了变化。
使用场景2:状态标记(顺序性),前面讲到的context加载的例子
使用场景3:单例设计模式中的Double-Check(顺序性)
5.volatile和synchronized
使用区别 | 原子性保证 | 顺序性保证 | 可见性保证 | 是否会使线程阻塞 | |
volatile |
只能修饰实例变量、类变量; 修饰的变量可以为null; |
不可以 | 可以 | 可以 | 不会 |
synchronized |
只能修饰方法或语句块; 同步语句块的monitor对象不能为null; |
可以,使用一种排他机制 | 可以,程序串行化执行来保证 | 可以,使用机器指令迫使其他线程工作内存数据失效 | 会 |
扩展知识点1:当int取值-1~5采用iconst指令,取值-128~127采用bipush指令,取值-32768~32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令。
扩展知识点2:查看java源文件字节码的其中一个方法,dos命令行cd到java文件目录,执行javac xx.java、javap -c xx.class命令,如javac HelloWorld.java, javap -c HelloWorld.class.
扩展知识点3:java的long和double赋值不是原子操作,因为先写32位,后写32位,非线程安全的,此时可以使用volatile关键字修饰,此处使用的是volatile的可见性,而不是原子性。
原文:https://www.cnblogs.com/lyrb/p/10720735.html