内存可见性
- 可见性:如果一个线程对共享变量值的修改,能够及时的被其他线程看到,那么这个共享变量就是可见的
- 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量
JAVA内存模型 JMM(Java Memory Model)
JMM描述了java程序中各种变量(就是指线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节
- 主内存:整个JVM管理的内存
-
工作内存:独属于每个线程的内存,保存该线程用到的所有变量的副本(即主内存中该变量的一个copy)
- 线程只能与自己的工作内存打交道,对共享变量的所有操作不能在主内存中直接读写,必须在自己的工作内存中进行操作,注意,是read and write
- 如果希望与主内存交互,那么必须先操作本身的工作内存中变量,然后通过工作内存与主内存的交互达到目的
- 再强调一点,线程只能与自己的工作内存交互,不能访问其他线程的工作内存。
- 如果需要在工作内存中传递变量值,需要通过主内存作为桥梁处理。
共享变量内存可见性的实现原理
如果线程1修改后的共享变量A想被线程2看到,那么需要经历以下步骤:
- 将工作内存1中修改过的A最新值 刷新到主内存中
- 将主内存中被修改过的A最新值更新到工作内存2中
如果在任何一个步骤中出现问题,都会导致数据在不同的内存区域存在不同的值,也就是所谓的线程不安全。
java语言层面的实现可见性的方式:
- synchronized
- volatile
jdk1.5之后引入的concurrent下面的包属于另一种实现方式
可见性保证前提:
- 线程修改后的共享变量能及时的刷新到主内存中
- 其他线程能够及时的把共享变量-最新值从主内存更新到自己的工作内存中
synchronized
特性:
- 原子性---—即同步,保证同一时间只有一个线程可以访问锁内代码
- 可见性
JMM关于synchronized的两条规定
- 线程解锁前,必须把共享变量的最新值刷新到主内存中
- 线程加锁时,将清空工作内存中存储的共享变量的值,从而使用共享变量时,必须从主内存中重新读取最新的值。(注意:解锁和加锁,是指同一把锁)
满足以上两条规定,也就意味着解锁前对共享变量的值的更新可以在,下次加锁时对其他线程是可见的。
但是即使没加入synchronized修饰,主内存和工作内存之间的数据更新也不一定不会发生,因为cpu缓存的刷新是非常快的,只有在高并发的情况下才会出现线程不安全的情况。
线程执行互斥代码的过程如下:
- 获取互斥锁
- 清空工作内存,把本线程所有工作内存中的共享变量都清除
- 从主内存copy变量的最新副本到工作内存
- 开始执行代码
- 如果有更新,则把更改后的共享变量值刷新到主内存中
- 释放互斥锁
指令重排序
代码书写的顺序和实际执行的顺序可能不同,指令重排序是编译器JIT或者处理器JVM为了提高程序性能而做的优化,主要有三种:
- 编译器优化的重排序,由编译器优化,主要在单线程环境下,在保证结果正确性之前对代码的执行顺序进行调整
- 指令级并行重排,处理器级别优化,cpu支持指令级并行技术,多核
- 内存系统的重排序,主要是处理器对读写缓存进行的优化,也就是上面说的主内存、工作内存之类的操作
as-if-serial
指无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致。
java编译器、运行时(RunTime,JVM)和处理器都会保证java在单线程下遵循as-if-serial语义
所以指令重排序不会导致单线程下出现内存可见性问题。
但是在多线程中,如果代码交错执行,那么就有可能出现可见性问题
只有数据依赖相关才会禁止重排序,逻辑上控制,比如if语句和if中的逻辑,也可能会出现重排序
导致共享变量在线程间不可见的原因
- 线程的交叉执行
- 重排序+线程的交叉执行
- 共享变量更新后的值没有在工作内存与主内存之间即使更新
synchronized的解决方案
- —>原子性,synchronized修饰的代码在一定时间内只能由一个线程持有,线程释放锁之后才会被其他线程占用
- —>原子性,重排序只能在单线程内部排,不会出现3.1—> 4.2 这种情况的重排
- —> synchronized 可见性保证,释放锁的时候会刷新到主内存
代码:
package com.alan.alanstatemachine;
import org.junit.Test;
import java.lang.Thread;
/**
* 用于学习synchronized使用方法
*/
public class SynchronizedDemo {
// 首先定义三个变量, 都是共享变量
boolean ready = false;
int number = 2;
int result = 0;
/**
* 写方法,更改共享变量的值
*/
public void write() {
ready = true; // 步骤1.1
number = 4; // 步骤1.2
}
/**
* 读方法,打印共享变量的值
*/
public void read() {
if (ready) { // 步骤2.1
result = number * 3; // 步骤2.2
}
System.out.println("current result = " + result);
}
/**
* sync写 方法,更改共享变量的值
*/
public synchronized void writeWithSync() {
ready = true; // 步骤3.1
number = 6; // 步骤3.2
}
/**
* sync读 方法,打印共享变量的值
*/
public synchronized void readWithSynv() {
if (ready) { // 步骤4.1
result = number * 3; // 步骤4.2
}
System.out.println("sync current result = " + result);
}
// 创建一个内部线程类,用于启动多个线程测试内存可见性
class ReadAndWriteThread extends Thread {
boolean flag = false;
ReadAndWriteThread(boolean outFlag) {
this.flag = outFlag;
}
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
*/
@Override
public void run() {
if (flag) {
write();
writeWithSync();
} else {
read();
readWithSynv();
}
}
}
@Test
public void test() {
SynchronizedDemo demo = new SynchronizedDemo();
// 传入true,应该是写操作,修改工作内存中的共享变量值
demo.new ReadAndWriteThread(true).start();
// 传入false,读操作,看是否拿到了最新的修改后 的ready及number、result数据
demo.new ReadAndWriteThread(false).start();
// 保证可见性的情况
// 如果执行顺序是 1.1 --> 2.1 --> 2.2 --> 1.2 那么结果是6
// 如果执行顺序是 1.1 --> 1.2 --> 2.1 --> 2.2 那么结果是12
// 如果重排序1,执行顺序是1.2 --> 2.1 --> 2.2 --> 1.1,那么结果是0
}
}
Volatile保证内存可见性
- Volatile可以保证共享变量的可见性
- 但是并不能保证原子性
- volatile通过内存屏障和禁止指令重排序来实现内存可见性
- 线程在对volatile修饰的变量进行写操作之后,处理器会在写操作后加入一个store的屏障指令,会把处理器写缓冲区的缓存强制刷新到主内存中去,所以主内存中的变量值就是最新值。(??但是,不是写在工作内存中的吗?跟处理器缓存有啥关系?难道还是通过处理器缓存来实现的刷新同步么?)—注意,工作内存只是一个逻辑概念,并不真实存在,它是由写寄存器+写缓冲区实现的
- store的屏障指令还能防止处理器将volatile修饰变量之前的操作重排序到volatile修饰变量之后
- 对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,也会强制缓冲区缓存失效,从而读到主内存中变量的最新值。同时load屏障指令也有禁止指令重排序的效果,不是禁止所有的,只是禁止volatile变量之前和之后的操作位置互换
所以线程对volatile变量的操作步骤就如下:
写:
- 改变工作内存中共享变量副本的值
- 将最新值及时刷新到主内存中去
读:
- 失效工作内存中变量,强制从主内存中读取最新的变量值存入工作内存中,作为副本存在
- 使用时从工作内存中读取。
JMM中定义了8条指令来完成主内存和工作内存的数据同步操作
// TODO
Volatile不能保证原子性
private int number = 1;
number++;
其中number++并不是原子操作,它是以下三个分解操作的简写:
1. get number value from number_var
2. number_value + 1 to a temp var
3. set new number value to number_var
对于synchronized,
synchronized(this){
number++;
}
由于synchronized的语言特性,number++语句此时是一个原子操作
而对于volatile:
private volatile int number = 1;
number++;
无法保证原子性
示例代码:
/**
* volatile为什么不能保证对共享变量的原子性操作
*/
public class VolatileDemo {
private volatile int number = 0;
/**
* increase
*/
public void increase() {
// 可见性是肯定的
// 为了更直观看到无法保证原子性,休眠下
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
number++;
}
/**
* get number
*
* @return current number value
*/
public int getNumber() {
return number;
}
@Test
public void test() {
// 刚开始线程数
System.out.println("start thread count =" + Thread.activeCount() );
int startThreadCount = Thread.activeCount();
// 启动线程增加volatile变量
for (int i = 0; i < 500; i++) {
new Thread(() -> increase()).start();
}
// 如果还有子线程未执行完,则主线程通过yield()让出cpu资源
while (Thread.activeCount() > startThreadCount) {
System.out.println("current sub thread count=" + Thread.activeCount());
Thread.yield();
}
System.out.println("current number value =" + getNumber());
}
}
可以看下,基本上getNumber的值最后都是小于500的。问题就出在number++上
因为volatile无法保证number++的三个分解操作的原子性,所以可能同时有三个线程过来操作这三个操作,整个过程就会串掉。
假设某一时间点 number = 5:
- 线程A获取到cpu资源,获取到number的值,然后释放掉cpu
- 线程B获取到cpu资源,获取到nubmer的值
- 线程B number +1
- 线程B 将number写入到工作内存,由于volatile修饰了number变量,那么在主内存及线程B的工作内存中,number = 6
- 但是线程A的工作内存中,number = 5,因为读取的时候确实是从主内存中读取的最新值,那么在后续操作的时候不需要再去主内存中再读一次
- 此时如果线程A再获取到cpu资源,执行+1操作,并写入工作内存中,那么此时number = 6,再写入到主内存中,也还是6
- 那么虽然对number进行了两次++操作,但是实际上在主内存中,只加了一个1
保证number原子性的解决方案有:
- 使用synchronized关键字修饰number++代码
- 使用ReentrantLock(java.util.concurrent.locks)
- 使用AtomicInteger (java.util.concurrent.atomic)
修改后的代码:
import org.junit.Test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
- volatile为什么不能保证对共享变量的原子性操作
*/
public class VolatileDemo {
private volatile int volatileNumber = 0;
// 使用synchronized,不需要再使用volatile来保证可见性
private int synchronizedNumber = 0;
// 使用reentrantLock
private int lockNumber = 0;
private Lock lock = new ReentrantLock();
/**
- increase
*/
public void volatileIncrease() {
// 可见性是肯定的
// 为了更直观看到无法保证原子性,休眠下
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.volatileNumber++;
}
public void synchronizedIncrease() {
// 可见性是肯定的
// 为了更直观看到无法保证原子性,休眠下
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 为什么不在方法定义上加synchronized呢?
// 其实也可以,但是这样锁的粒度比较大,休眠也被锁住了,无法释放资源,等待时间就会比较久
// 所以在代码块的基础上加synchronized关键字
synchronized (this) {
this.synchronizedNumber++;
}
}
public void lockIncrease() {
// 可见性是肯定的
// 为了更直观看到无法保证原子性,休眠下
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用reentrantLock
lock.lock();
try {
this.lockNumber++;
} finally {
lock.unlock();
}
}
/**
- get number
*
- @return current number value
*/
public int getVolatileNumber() {
return this.volatileNumber;
}
public int getSynchronizedNumber() {
return this.synchronizedNumber;
}
public int getLockNumber() {
return this.lockNumber;
}
@Test
public void test() {
// 刚开始线程数目
System.out.println("start thread count =" + Thread.activeCount() );
int startThreadCount = Thread.activeCount();
// 启动线程增加volatile变量
for (int i = 0; i < 500; i++) {
new Thread(() -> volatileIncrease()).start(); // volatile 肯定不准
new Thread(() -> synchronizedIncrease()).start(); // synchronized 为啥这个也不准呢?
new Thread(() -> lockIncrease()).start(); // Lock 为啥你也不准呢?
}
// 如果还有子线程未执行完,则主线程通过yield()让出cpu资源
while (Thread.activeCount() > startThreadCount) {
Thread.yield();
}
System.out.println("current volatile number value =" + getVolatileNumber());
System.out.println("current synchronized number value =" + getSynchronizedNumber());
System.out.println("current lock number value =" + getLockNumber());
}
}
volatile使用的场景
要在多线程中安全的使用volatile变量,需要满足以下两个条件:
- 对变量的写操作不依赖其当前值
- 比如 count++,count=count+1这种,就不满足
- 而对于boolean类型、温度变化场景,就满足
- 该变量不能包含在其他变量的不变式中
- 比如有两个volatile变量,low 和 up,如果存在 low < up 的这种不变式比较,则不满足
Synchronized VS volatile
- volatile不需要加锁,不会阻塞线程,所以相对synchronized 更轻量级,性能更高
- 从内存可见性角度讲,对volatile变量的读操作,相当于synchronized的加锁,即清空工作内存,从主内存中更新最新值到工作内存中
- 而对volatile的写操作,相当于synchronized的解锁,将数据及时刷新到主内存中
- synchronized同时保证可见性及原子性,而volatile只保证可见性
补充:
java中long和double都是64位的,而对这两种类型对象的操作可能并不是原子操作,因为jmm允许jvm对没有添加volatile修饰的64位对象操作分解为两次32位读写操作来操作,所以加上volatile可以解决这种问题。不过大多数情况下,多数jvm都已经实现了原子性,所以不需要特殊操作。
另一种保证内存可见性的操作,是使用final修饰,这个后续再细看。