先来分享一篇写的非常好的关于 volatile 博客《Java并发编程:volatile关键字解析》,里面也详细的介绍了原子性、可见性和有序性,再来谈一谈自己的一些理解:
先来说一下 JMM:
JMM
JMM ( Java内存模型 Java Memory Model)本身是一种抽象的概念,他并不真实存在;他描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括 实例字段,静态字段和构成数组对象的元素)的访问方式。
速度:
硬盘 < 内存 < CPU(会有高速缓存区)
CPU的速度会远大于内存的读写速度,CPU 直接读取内存的数据,就会导致CPU 会有部分时间闲置,导致资源的浪费;所以CPU会有一个高速缓存区。
JMM关于同步的规定:
1、线程解锁前,必须把共享变量的值刷新回主内存;
2、线程加锁前,必须读取主内存的最新值到自己的工作内存;
3、加锁解锁是同一把锁。
由于JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定的所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,不同的线程间是无法访问对方的工作内存的,线程间的通信必须通过主内存来完。
volatile关键字
并发编程中又是那个概念:原子性、可见性、有序性;并发程序如果想要正确地执行,必须要保证原子性、可见性以及有序性。
volatile 是 Java 虚拟机提供的轻量级的同步机制,它能够保证【有序性】和【可见性】,但是不能够保证【原子性】,所以是无法替代 synchronized 的。
在性能方面,volatile 是一种非锁机制,这种机制可以避免锁机制引起的线程上下文切换和调度问题。因此,volatile 的执行成本比 synchronized 更低。
Java 中,可以使用 synchronized 和 Lock 保证【原子性】和【可见性】,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,所以不存在会被其他线程中途获取结果的问题。
一、可见性
假设线程1、线程2同时从主内存中拿到了 i 的值为 10,并都对 i 进行 i = i + 5 的操作;当线程1 修改了 i 的值为15 并将 15 写入主内存中,此时 线程2 并不知道 i 的值已经被修改,他的工作内存中的 i 还是10,所以此时线程2对 i 执行 +5 的操作也会返回 15 并写入到主内存,所以最终两个线程执行完毕后主内存 i 的值将仍然是 15;如果执行完毕后,可以通知线程2,线程2拿来线程1的结果再进行 +5 的操作,就可以得到正确的执行结果。
可见性,即多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
Java 中,volatile 关键字会强制将修改的值立即写入主存,如果有其他线程需要读取时,它会从内存中读取新值;普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值。
被 volatile 关键字修饰的共享变量在转换成汇编语言时,会加上一个以 lock 为前缀的指令,当 CPU 发现这个指令时,立即做两件事:
1.将当前内核高速缓存行的数据立刻回写到内存;
2.使在其他内核里缓存了该内存地址的数据无效。
public static void main(String[] args ){
Test test = new Test();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始执行!");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.addFive();
System.out.println(Thread.currentThread().getName() + " y的值为:" + test.y);
System.out.println(Thread.currentThread().getName() + "执行结束!");
},"test01").start();
while (5 == test.y){
}
System.out.println(Thread.currentThread().getName() + " y的值为:" + test.y);
}
private static class Test{
int y = 5;
public void addFive(){
y = y + 5;
}
}
如果 y 不加 volatile 修饰,while 将会一直循环:
test01开始执行!
test01 y的值为:10
test01执行结束!
加上 volatile 后,执行结果为:
test01开始执行!
test01 y的值为:10
test01执行结束!
main y的值为:10
二、原子性
原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
Java中,基本数据类型变量的读取和赋值操作是原子性操作,例如:x = 10 ,就是直接将 10 赋值给 x,是原子操作 ,写入当前线程的工作内存中,再同步给主存;x++ ; y = x 等是不具有原子性的;
public static void main(String[] args ){
Test test = new Test();
for (int i = 1;i <= 20;i++){
new Thread(() -> {
for (int y = 1;y <= 1000;y++){
test.addOne();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
// 2 表示 一个 main 线程,一个 gc 线程,表示其他线程全部执行结束
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " y的值为:" + test.y);
}
private static class Test{
volatile int y = 0;
public void addOne(){
y++;
}
}
返回结果:
main y的值为:19563
所以,volatile 是不保证原子性的,此处如果想保证原子性,可以使用 synchronized 或者是使用:
AtomicInteger y = new AtomicInteger();
public void addOne(){
y.addAndGet(1);
}
三、有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,因为对有依赖性的内容并不会进行重排,例如:
int y = 10;
int x = y; // x 依赖于 y,所以不会对这两行代码进行重排序
但是多线程并发执行是无法保证正确性的。
jdk1.5 中,对 volatile 的语义进行了增强,提供了可靠的有序性的支撑,加上 volatile 是添加一个内存屏障(是一个 CPU 指令,可以保证特定操作的执行顺序;保证某些变量的内存可见性。),禁止了重排序,在写操作后加一条 store 屏障指令,将工作内存中的共享变量的值刷新回主内存;在读操作前添加 load 屏障指令,从主内存中读取共享变量。
说到有序性,还有一个 happens-before 规则 也需要了解一下,我们可以通过 happens-before 规则推断出是否能够满足有序性。
happens-before 规则
因为 Jvm 会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,需要 happens-before 规则定义一些禁止编译优化的场景,保证并发编程的正确性。
1、程序的顺序规则:
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;编译器仍然会对代码机型重排序,但是保证结果一定等于顺序推演的结果。
2、volatile规则:
如果一个线程先去写一个 volatile 变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
3、传递性规则:
就是happens-before原则具有传递性,即 A happens-before B , B happens-before C,那么 A happens-before C。
4、管程中的锁规则:
无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)。
5、线程 start() 规则:
在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
6、线程 join() 规则:
在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
7、线程中断规则:
对线程 interrupt() 方法的调用 先行发生于 被中断线程代码检测到中断事件的发生,可以通过 Thread.interrupted() 检测到是否发生中断。
8、对象终结规则
一个对象的初始化完成先行发生于他的 finalize() 方法的开始。
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1 线程A修改共享变量为 1
flag = true; // 2 线程A修改volatile变量为 true
}
public void reader() {
if (flag) { // 3 线程B读同一个volatile变量 为 true
int i = a; // 4 线程B读共享变量 i = 1
}
}