Java多线程之内存可见性

Posted by JackPeng on June 20, 2016

多线程背景知识介绍

1 多线程基础概念

**进程与线程 **

进程 进程程序(任务)的执行过程(动态性),持有资源(共享内存,共享文件)和线程,是资源和线程的载体。

线程 进程的最小执行单元,比如将一个班级视为进程,则班里的每一个学生就是线程,所有学生共享黑板,教师等。

线程的交互:互斥与同步,公共资源有限,需要竞争。

2 可见性

什么是可见性?

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

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

工作内存:java内存模型抽象出的概念。

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

(1) 所有的变量都存储在住内存中;

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

image

两条规定: (1) 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;

(2) 不同线程无法直接访问其他线程工作内存中的变量,线程之间传递变量值需要通过主内存完成。

共享变量可见性实现原理

线程1对共享变量的修改要被线程2看到,需要下边两个步骤:

(1) 把线程1工作内存中修改后的共享变量刷新到主内存中;

(2) 把主内存中更新过的共享变量更新到工作内存2中。

image

3 synchronized实现可见性

JMM关于synchronized的两条规定:

(1) 线程解锁前,必须把共享变量最新值刷新到主内存中;

(2) 线程加锁时,将清空工作内存中共享变量的值,使用共享变量时,重新从主内存中读取最新值(加锁解锁需要是同一把锁)

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

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

(1)获得互斥锁;

(2) 清空工作内存;

(3) 从主内存拷贝共享变量副本到工作内存;

(4) 执行代码;

(5)将修改后的共享变量刷新到主内存;

(6) 释放互斥锁。

补充2个知识点:重排序

重排序

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

(1)编译器优化的重排序;(编译器优化)

(2)指令集并行重排序(处理器优化);

(3) 内存系统的重排序(处理器优化)。

as-if-serial

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

```
int num1=1;
int num2=2;
int sum=num1+num2;
```

单线程:第1,2行的顺序可以重排,但第3行不能,重排序不会给单线程带来内存可见性的问题;

多线程中程序交错执行时,重排序可能会造成内存可见性问题。

下面的代码演示内存可见性问题:

```

public class SynchronizedDemo {
//共享变量
private boolean ready = false;
private int result = 0;
private int number = 1;
//写操作
public void write(){
	ready = true;	      				 //1.1
	number = 2;		                    //1.2
}
//读操作
public void read(){
	if(ready){						     //2.1
		result = number*3;	 	//2.2
	}
	System.out.println("result的值为:" + result);
}

//内部线程类
private class ReadWriteThread extends Thread {
	//根据构造方法中传入的flag参数,确定线程执行读操作还是写操作
	private boolean flag;
	public ReadWriteThread(boolean flag){
		this.flag = flag;
	}
	@Override
	public void run() {
		if(flag){
			//构造方法中传入true,执行写操作
			write();
		}else{
			//构造方法中传入false,执行读操作
			read();
		}
	}
}

public static void main(String[] args)  {
	SynchronizedDemo synDemo = new SynchronizedDemo();
	//启动线程执行写操作
	synDemo .new ReadWriteThread(true).start();
	try {
		Thread.sleep(1000);
	} catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	//启动线程执行读操作
	synDemo.new ReadWriteThread(false).start();
  }
}

```

可能的结果 (1) 1.1->2.1->2.2->1.2 result:3;

(2) 1.1->2.1->1.2->2.2 result:6;

(3) 1.2->2.1->2.2->1.1 result:0.

可见性分析

导致共享变量在线程间不可见的原因: synchronized解决方案

(1)线程的交叉执行; 原子性

(2) 重排序结合线程交叉执行; 原子性

(3) 共享变量更新后没有在工作内存与主内存间及时更新。 可见性

安全的代码

```
//写操作
public synchronized void write(){
	ready = true;	      				 //1.1
	number = 2;		                    //1.2
}
//读操作
public synchronized void read(){
	if(ready){						     //2.1
		result = number*3;	 	//2.2
	}
	System.out.println("result的值为:" + result);
}
```

synchronized既可以保证原子性,也可以保证可见性。

4 volatile实现可见性

volatile如何实现内存可见性:

深入来说,通过加入内存屏障和禁止重排序优化来实现的:

对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,强制刷新到主内存;

对volatile变量执行读操作时,会在读操作前加入一条load指令,强制从主内存读取。

java中volatile相关的指令共有8个,store和load是其中两个,具体可自行百度。

但是,volatile不能保证原子性

以num++为例,这行代码并非原子操作,而是分为3步:

(1) 读取num;

(2) num+1;

(3) 写入num。

加入有A ,B两个线程同时对num进行num++操作:volatile int num=5;

(1)线程A读取num; (2)线程B读取num; (3)线程B对num+1; (4)线程B写入最新的num值6; (5)线程A中的num仍然为5,执行+1操作后为6,写入主内存6

结果为6,期望值为7.这就是volatile不能保证原子性造成的。

解决方法

保证num自增操作的原子性:

(1) 使用synchronized关键字;

(2) 使用ReentrantLock;

(3) 使用AtomicInteger。

volatile适用场合

要在多线程中安全的使用volatile变量,必须同时满足:

(1) 对变量的操作不依赖其当前值:

 不满足:count++;count=count+1;
 
 满足:Boolean变量,温度值等。

(2) 该变量没有包含在具有其他变量的不变式中:

 不满足:不变式,low<up.

volatile不需要加锁,不会阻塞线程,效率高于synchronized,但只能保证内存可见性,无法保证原子性。

### synchronized和volatile的比较

volatile比synchronized更轻量级;

volatile没有synchronized使用广泛。