本文主要是介绍Java多线程之volatile关键字,happens-before,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 1 volatile
- 1.1 理解
- 1.2 缓存
- 2 happens-before原则
- 2.1 JMM内存模型
- 2.2 重排序
- 2.3 什么是happens-before
- 2.4 具体的规则
1 volatile
1.1 理解
Java
语言包含两种内在的同步机制:同步块(或方法)和 volatile
变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile
变量的同步性较差(但有时它更简单并且开销更低
),而且其使用也更容易出错。
之所以要单独提出volatile
这个不常用的关键字原因是这个关键字在高性能的多线程程序中也有很重要的用途,只是这个关键字用不好会出很多问题。
首先考虑一个问题,为什么变量需要volatile
来修饰呢?
要搞清楚这个问题,首先应该明白计算机内部都做什么了。比如做了一个i++
操作,计算机内部做了三次处理:读取-修改-写入
。
同样,对于一个long
型数据,做了个赋值操作,在32
系统下需要经过两步才能完成,先修改低32位,然后修改高32
位。
假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题,是这些步骤执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。
通过这个设想,就不难理解volatile
关键字了。
volatile
可以用在任何变量前面,但不能用于final
变量前面,因为final
型的变量是禁止修改的。也不存在线程安全的问题。
对于volatile, <The Java Language Specification Third Edition>
是这样描述的
“A field may be declared volatile, in which case the Java memory model ensures that all threads see a consistent value for the variable.”
“… the volatile modifier guarantees that any thread that reads a field will see the most recently written value.” - Josh Bloch
意思是,如果一个变量声明为volatile
,Java
内存模型保证所有的线程看到这个变量的值是一致的。
Josh Bloch 说 ”volatile描述符保证任意一个程序读取的是最新写的值“
点击此处了解更多关于volatile知识
1.2 缓存
有人会问,内存不是存放变量值的地方吗,线程T1
写,然后线程T2
读,怎么会出现不一致的情况呢。
缓存
实际上内存不是唯一存储变量的地方。CPU
往往会把变量的值存放到缓存中。假如一个CPU
,即使在多线程环境下也不会出现值不一致的情况。但是,在多CPU
,或者多核CPU
的情况就不是这样了。
如下图所示,在多个CPU
情况下,每个CPU
都有独立的缓存,CPU
通过连接相互获取缓存内容。线程T1
的可能运行在CPU 0
上,它从内存中读取值放到缓存中做运算,比如执行方法foo
;线程T2
运行于CPU 1
上,执行方法bar
。
void foo(void)
{a = 1;b = 1;}void bar(void)
{while (b == 0) continue;assert(a == 1);
}
在多CPU
情况下,由于CPU
各自缓存的原因,线程可能观察到不一致的变量值。
volitate
标志通过CPU
基本的指令,比如(mfence x86 Xeon 或 membar SPARC
)添加内存界限,让缓存和内存之间的值进行同步。查了某些资料说是总线嗅探机制
,当一个线程改变了内存中的值,其他线程监听到这个变量的值变了后,就会自动同步内存中最新的值
volatile
的一个作用
由于volatile
保证一些线程写的值,另外一些线程能够立即看得到。我们可以通过这一特性,实现信号或事件机制。比如下面程序里主线程可以发送信号(把stopSignal
设为true
), 把线程workerThread
立即终止。
public class WorkerOwnerThread{// field is accessed by multiple threads.private static volatile boolean stopSignal;private static void doWork(){while (!stopSignal){Thread t = Thread.currentThread(); System.out.println(t.getName()+ ": I will work until i get STOP signal from my Owner...");}System.out.println("I got Stop signal . I stop my work");}private static void stopWork() {stopSignal = true;//Thread t = Thread.currentThread(); //System.out.println("Stop signal from " + t.getName() );}public static void main(String[] args) throws InterruptedException {Thread workerThread = new Thread(new Runnable() {public void run() {doWork(); }});workerThread.setName("Worker");workerThread.start();//Main threadThread.sleep(100);stopWork();System.out.println("Stop from main...");}
}
2 happens-before原则
从JDK5
开始,java
使用新的JSR -133
内存模型。JSR-133
提出了happens-before
的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before
关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间
happens-before
是JMM
最核心的概念,所以在了解happens-before
原则之前,首先需要了解java
的内存模型。
2.1 JMM内存模型
java
内存模型是共享内存的并发模型,线程之间主要通过读-写
共享变量来完成隐式通信
。java
中的共享变量是存储在内存中的,多个线程由其工作内存,其工作方式是将共享内存中的变量拿出来放在工作内存,操作完成后,再将最新的变量放回共享变量,这时其他的线程就可以获取到最新的共享变量。
从横向去看看,线程A和线程B就好像通过共享变量在进行隐式通信
。这其中有很有意思的问题,如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了 脏读
现象。
为避免脏读,可以通过同步机制
(控制不同线程间操作发生的相对顺序)来解决或者通过volatile
关键字使得每次volatile
变量都能够强制刷新到主存,从而对每个线程都是可见的。
点击了解更多Java内存模型知识点
2.2 重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:如图,1属于编译器重排序
,而2和3统称为处理器重排序
。
这些重排序会导致线程安全的问题,一个很经典的例子就是DCL
问题。JMM
的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序。
(1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
(2)指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
(3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
2.3 什么是happens-before
JMM
可以通过happens-before
关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before
关系,尽管a操作和b操作在不同的线程中执行,但JMM
向程序员保证a操作将对b操作可见)。
具体的定义为:
1)如果一个操作happens-before
另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before
关系,并不意味着Java平台的具体实现必须要按照happens-before
关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before
关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM
允许这种重排序)。
2.4 具体的规则
程序顺序规则
:一个线程中的每个操作,happens-before
于该线程中的任意后续操作。监视器锁规则
:对一个锁的解锁,happens-before
于随后对这个锁的加锁。volatile
变量规则:对一个volatile
域的写,happens-before
于任意后续对这个volatile
域的读。传递性
:如果A happens-before B
,且B happens-before C
,那么A happens-before C
。start()
规则:如果线程A
执行操作ThreadB.start()
(启动线程B),那么A线程的ThreadB.start()
操作happens-before
于线程B
中的任意操作。Join()
规则:如果线程A
执行操作ThreadB.join()
并成功返回,那么线程B中的任意操作happens-before
于线程A从ThreadB.join()
操作成功返回程序中断规则
:对线程interrupted()
方法的调用先行于被中断线程的代码检测到中断时间的发生。- 对象
finalize
规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()
方法的开始。
注意
:两个操作之间具有happens-before
关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before
仅仅要求前一个操作(执行结果
)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second
)
这篇关于Java多线程之volatile关键字,happens-before的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!