Java内存可见性-synchronized和volatile

2/22/2017来源:ASP.NET技巧人气:1886

java内存可见性-synchronized和volatile

1. 基本概念

可见性:一个线程对共享变量值的修改,能够及时的被其它线程看到。

共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。

在Java内存模型(Java Memory Model)中描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。

所有的变量都存储在主内存中 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)

Java内存模型

Java内存模型

在这里,有2条规定: - 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主存中读写 - 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

2. 共享变量可见性实现的原理

线程1对共享变量的修改,要想被线程2及时看到,必须要经过如下两个步骤:

把工作内存1中更新过的共享变量刷新到主存中 将主内存中最新的共享变量的值更新到工作内存2中

步骤如下图所示: 1. 初始状态下,主内存中和所有线程的工作内存中的共享变量X的值都是0

初始状态 2. 线程1更改工作线程1中X的副本值为1

线程1更改工作线程1中X的副本值为1 3. 工作内存1将副本X的值刷新到主内存中

工作内存1将副本X的值刷新到主内存中 4. 主内存将最新的X的值1更新到工作内存2中

这里写图片描述

通过以上步骤,就保证了共享变量在不同线程的工作内存中的可见性。

3. synchronized实现内存可见性

3.1 synchronized能够实现:

原子性(同步) 可见性

3.2 JMM关于synchronized的两条规定:

线程解锁前,必须把共享变量的最新值刷新到主内存中 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需是同一把锁)

线程解锁前对共享变量的修改在下次加锁时对其它线程可见

3.3 线程执行互斥代码的过程

获得互斥锁 清空工作内存 从主内存拷贝共享变量的最新副本到工作线程 执行代码 将更改后的共享变量的值刷新到主内存 释放互斥锁

4. 重排序

重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化

编译器优化的重排序(编译器优化) 指令集并行重排序(处理器优化) 内存系统的重排序(处理器优化)

代码顺序是

int number = 1; int result = 0;

重排序后,执行顺序可能是

int result = 0; int number = 1;

5. as-if-serial语义

as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)

int num1= 1; //语句1 int num2= 2; //语句2 int num3 = num1 + num2; //语句3

单线程:语句1、2的顺序可以重排,但语句3不能 重排序不会给单线程带来内存可见性的问题 多线程中程序交错执行时,重排序可能造成内存可见性问题

导致共享变量在线程间不可见的原因 synchronized解决办法
线程的交叉执行 原子性
重排序结合线程交叉执行 原子性
共享变量更新后的值没有在工作内存与主内存之间及时更新 可见性

6. volatile实现内存可见性

volatile关键字: - 能够保证volatile变量的可见性 - 不能保证volatile变量符合操作的原子性

6.1 volatile如何实现内存可见性

深入来说:通过加入内存屏障和禁止重排序优化来实现 - 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令 - 对volatile变量执行读操作时,会在写操作后加入一条load屏障指令

通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中读取该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存中。这样任何时刻,不同的线程总能看到该变量的最新值

6.2 线程写volatile变量的过程

改变线程工作内存中volatile变量副本的值 将改变后的副本的值从工作内存刷新到主内存

6.3 线程读volatile变量的过程

从主内存中读取volatile变量的最新值到线程的工作内存中 从工作内存中读取volatile变量的副本

6.4 volatile不能保证volatile变量符合操作的原子性

int number = 0; number++; //不是原子操作

number++ 包含如下三个步骤: 1. 读取number的值 2. 将number的值加1 3. 写入最新的number值

加入synchronized,变为原子操作

synchronized(this) { number++ } volatile int number = 0; //变为volatile变量,无法保证原子性

6.5 volatile的适用场合

要在多线程中安全地使用volatile变量,必须同时满足: 1. 对变量的写入操作不依赖于其当前值 - 如:number++、count = count * 5等 2. 该变量没有包含在具有其他变量的不变式中 - 如:不变式low

7. synchronized和volatile比较

volatile不需要加锁,比synchronized更轻量级,不会阻塞线程 从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁 synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性