多线程(8)- 深入volatile关键字
前言
并发编程三特性:原子性、有序性、可见性,volatile关键字扩展:
概念
1、并发编程
1.1原子性
- 概念:在一次的操作或者多次操作中,要么所有操作全部都起到执行,要么所有操作都不执行。
- valitale不保证数据的原子性,synchronized保证,自JDK1.5版本起。其提供的原子类型变量也可以保证原子性。
1.2可见性
-
概念:当一个线程对一个共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。
-
Reader线程会将x从主内存读取到CPU Cache中,也就是从主内存缓存到线程的本地内存中,Updater线程对x的修改对Reader是不可见的。
1.3有序性
- 指令重排导致代码在运行时的指令执行顺序不一定是编码顺序
- JVM在运行程序时会进行指令重排,处理器对输入的代码指令进行优化,如果一个指令x在执行过程中需要用到指令y的执行结果,那么处理器会保证y在x之前执行。
public class ThreeSpecialClass {
public static final int init_value = 0;
public static final int MAX = 5;
private boolean initialized = false;
private Context context;
public static void main(String[] args) {
/***原子性:一个或多个操作不可分割 , 要么都成功要么都失败**/
int x = 0;
x++;// 实际是 x = 0 x+1 为x赋值x+1
/**可见性:**/
//因为CPU Cache模型原因, init_value会被加载到CPU Cache中,也就是线程的本地内存副本,导致线程读取的是副本的值,没有读取到主存的值
new Thread(() -> {
int local_value = init_value;
while (local_value < MAX) {
if (local_value != init_value) {
System.out.println(Thread.currentThread().getName() + " local_value =" + local_value + " x = " + init_value);
local_value = init_value;
}
}
}, "reader").start();
/**有序性:处理器为了提高效率会进行指令重排序 , 单线程不受影响,多线程并发时可能出现问题**/
int a = 0;
int b = 0;
a++;
b = 20;
}
public Context load() throws NamingException {
if (!initialized) {
/**有序性
* 以下进行指令重排,先执行了initialized = true 再执行初始化,但在未执行初始化之前
* 这时其它线程发现initialized为true而不去初始化context,直接返回context,此时context为null
*/
context = new InitialContext();
initialized = true;
}
return context;
}
}
2、JVM保证三大特性
- JVM-JMM满足一致内存访问效果
- JMM规定所有的变量都是存在于主内存(RAM),每个线程都有自己的工作内存或本地内存(这点像CPU Cache),线程对变量的所有操作都必须在工作内存中进行,不能直接对主内存操作,且线程间的工作内存或本地内存不可访问。
- 比如某个线程中对变量赋值 i=1; 必须在本地内存中修改i的值才能将其写入主内存中。
2.1JMM与原子性
-
x = 10 :原子性
-
y = x :非原子性
- 从主存读取x的值(如果当前执行线程工作内存中有x的值,则直接获取)
- 将执行线程工作内存中y的值修改为x的值
- 将y写入主内存
-
x ++ 等价于 x = x+ 1;非原子性
- 从主存读取x的值(如果当前执行线程工作内存中有x的值,则直接获取)
- 将执行线程工作内存中x的值加1操作
- 将x写入主内存
-
多个原子性操作在一起不是原子性
-
简单的读取与赋值是原子性,将一个变量赋值给另一个变量不是原子性
-
JMM只保证了基本读取和简单赋值是原子性
-
如果想使得某个片段具备原子性,需要使用
syncharonized
、或JUC的Lock
-
如果想要使得int类型自增操作具备原子性可使用JUC包下的原子封装类型
java.util.concurrent.atomic.*
-
volatile不具备保证原子性语义
2.2JMM与可见性
- Java提供了三种方法来提供可见性
- 使用关键字volatile,当一个变量被volatile修饰,对于共享变量的读操作会在主内存(当其他线程修改了该变量,会使该
变量在其它线程的工作内存失效
,所以必须从主内存获取),对于共享资源的写操作是先更新工作内存,修改结束后会立即刷新到主内存。 - 使用
synchronized
保证可见性,只有一个线程获取锁,还会确保在释放锁之前对变量的修改刷新到主内存中
。 - JUC提供的显示锁
Lock
也可以保证可见性,只有一个线程获取锁,还会确保在释放锁之前对变量的修改刷新到主内存中
。
- 使用关键字volatile,当一个变量被volatile修饰,对于共享变量的读操作会在主内存(当其他线程修改了该变量,会使该
2.3JMM与有序性
-
Java提供了三种保证有序性的方式
- volatile
- synchronized:同步机制
- Lock:同步机制
-
happens-before原则
:Java的内存模型具备一些天生的有序性规则,不需要任何同步手段就能保证有序性- 程序次序规则:在同一个线程内,代码按照编写时的规则执行
- 锁定规则:一个unlock操作要先发生于对同一个锁的lock操作,意思是无论单线程还是多线程,如果一个锁是lock状态,那么必须释放锁才能继续进行lock操作(针对同一个锁对象)
- vilatile变量规则:对一个变量的写操作要早于对这个变量之后的读操作,如果两个线程,一个线程此时在写,另一个线程此时在读,那么必须是写操作先执行。
- 传递规则:如果操作A在操作B前,操作B在操作C前,那么操作A一定在操作C前。传递性
- 线程启动规则:Thread对象的start()方法先发生于该对象的任何操作,只有start之后才能真正运行。
- 线程中断规则:对线程执行interrupt()方法肯定要优先于捕获到中断信号,意思是如果线程收到了中断信号,那么此前一定执行过interrupt方法。
- 线程的终结规则:线程的所有操作要先发生于线程的终止检测,线程的执行一定在线程死亡前。
- 对象的终结规则:一个对象初始化的完成先行发生于finalize()方法之前。
总结:volatile关键字具有保证顺序性的语义;
3、深入解析
3.1 volatile语义
- Reader从主存中读取init_value的值,加载到工作内存
- Updater将init_value加载到工作内存且修改init_value的值,并更新到主内存
- Reader工作内存中init_value的值失效
- 由于在工作内存中的值失效,再次从主存中读取
int x = 0 ;
int y = 1;
volatile int z = 20; //volatile保证执行到这句时,上面x y已经赋值好了,同理x++和y--必须在z赋值之后才执行。
x++;
y--;
if (!initialized) {
/**有序性
* 以下进行指令重排,可能导致context为null ,先执行了initialized = true
* 这时其它线程发现initialized为true而不去初始化context,导致context为null
*/
context = new InitialContext();
/**使用volatile修饰,保证在initialized = true时 上面代码已经执行完毕。
* 这里必须对initialized加volatile,同时保证可见性,防止重复初始化
* 对context加volatile可以保证content初始化在initialized=true之前执行,
* 但是无法保证content初始化原子性,而且无法保证initialized对其它线程可见
* */
initialized = true;
}
-
理解volatile不保证原子性
- i++ 操作对 i 使用volatile关键字
- A线程读取主内存i的值到工作内存为0
- A线程对i进行+1操作重新写到工作内存,但未写到主内存,此时CPU时间片切换到线程B
- 因为A线程未写到主内存,所以线程B读取主内存i为0,现对i加1且更新到主内存,此时i=1
- 线程切换A,A线程将工作内存i的值写入到主内存也为1
- 造成AB两个线程都累加但是最终主内存结果为1
public class ThreeSpecialClass {
public static final int init_value = 0;
public static final int MAX = 5;
public volatile static int count = 0;
private static boolean initialized = false;
private static Context context;
public static void main(String[] args) throws InterruptedException, NamingException {
/***原子性:一个或多个操作不可分割 , 要么都成功要么都失败**/
//atomic();
/**可见性:**/
//visiable();
//有序性
//orderliness();
//volatile非原子性
testVolatileNoAtomicity();
}
private static void atomic() {
int x = 0;
x++;// 实际是 x = 0 x+1 为x赋值x+1
}
private static void visiable() {
//因为CPU Cache模型原因, init_value会被加载到CPU Cache中,也就是线程的本地内存副本,导致线程读取的是副本的值,没有读取到主存的值
new Thread(() -> {
int local_value = init_value;
while (local_value < MAX) {
if (local_value != init_value) {
System.out.println(Thread.currentThread().getName() + " local_value =" + local_value + " x = " + init_value);
local_value = init_value;
}
}
}, "reader").start();
}
public static Context orderliness() throws NamingException {
/**有序性:处理器为了提高效率会进行指令重排序 , 单线程不受影响,多线程并发时可能出现问题**/
if (!initialized) {
/**有序性
* 以下进行指令重排,可能导致context为null ,先执行了initialized = true
* 这时其它线程发现initialized为true而不去初始化context,导致context为null
*/
context = new InitialContext();
/**使用volatile修饰,保证在initialized = true时 上面代码已经执行完毕。
* 这里必须对initialized加volatile,同时保证可见性,防止重复初始化
* 对context加volatile可以保证content初始化在initialized=true之前执行,
* 但是无法保证content初始化原子性,而且无法保证initialized对其它线程可见
* */
initialized = true;
}
return context;
}
public static void testVolatileNoAtomicity() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
count++;
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(count);
}
}
3.2 volatile原理和实现机制
- OpenJDK下unsafe.cpp源码的阅读,被volatile修饰的变量存在
lock;
的前缀,相当于一个内存屏障
,会为指令的执行提供如下保障
- 确保指令重排不会将其后面的代码排到内存屏障之前
- 确保指令重排不会将前面的代码排到屏障之后
- 确保执行时,屏障前面的代码都执行完毕
- 强制将线程工作内存中的值修改刷新到主内存
- 如果是写操作,则会导致其他线程工作内存(CPU Cache)中的缓存数据失效
3.3 volatile使用场景
while (!isShutdown && ! isInterrupted()) {
try {
timeUnit.sleep(keepAliveTime);
} catch (InterruptedException e) {
shutdown();
break;
}
}
public static Context orderliness() throws NamingException {
/**有序性:处理器为了提高效率会进行指令重排序 , 单线程不受影响,多线程并发时可能出现问题**/
if (!initialized) {
/**有序性
* 以下进行指令重排,可能导致context为null ,先执行了initialized = true
* 这时其它线程发现initialized为true而不去初始化context,导致context为null
*/
context = new InitialContext();
/**使用volatile修饰,保证在initialized = true时 上面代码已经执行完毕。
* 这里必须对initialized加volatile,同时保证可见性,防止重复初始化
* 对context加volatile可以保证content初始化在initialized=true之前执行,
* 但是无法保证content初始化原子性,而且无法保证initialized对其它线程可见
* */
initialized = true;
}
return context;
}