说明:多线程的内存可见性涉及到多线程间的数据争用,也涉及到了多线程间的数据可见性
一、共享变量在线程间的可见性
1、可见性介绍:
可见性: 一个线程对共享变量值的修改,能够及时地被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
2、Java内存模型(JMM)
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
在内存模型中:
内存模型图:
JMM线程操作内存的两条基本的规定:
3、共享变量可见性实现的原理:
线程1对共享变量的修改要想被线程2及时看到,必须要经过如下2个步骤:
- 把工作内存1中更新过的共享变量刷新到主内存中
- 把住内存中最新的共享变量的值更新到工作内存2中
流程图:
二、synchronized实现可见性
由前面可以知道,要实现共享变量的可见性,必须保证两点:
- 线程修改后的共享变量值能够及时从工作内存刷新到主内存中;
- 其他线程能够及时把共享变量的最新值从住内存更新到自己的工作内存中
1、可见性的实现方式
【1】Java语言层面支持的可见性实现方式:
【2】synchronized能够实现的 两个功能:
【3】JMM关于synchronized 的两条规定:
线程解锁前对共享变量的修改在下次加锁时对其他线程可见
【4】线程执行互斥代码的过程:
【5】指令重排序的概念以及类型
重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化
示例:指令重排序 可能 造成的结果
【6】 as-if-serial
as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循 as-if-serial 语义)
int num1 = 1; //第1行代码
int num2 = 2; //第2行代码
int sum = num1 + num2 ; //第3行代码
单线程: 第1、2行的顺序可以重排,但第3行不能
重排序不会给单线程带来内存可见性问题
多线程中程序交错执行时,重排序可能会造成内存可见性问题
2、synchronized实现可见性代码
package mkw.demo.syn;
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();
}
}
3、synchronized实现可见性分析:
【1】没有进行重排序
执行结果:result的值为3
【2】进行重排序
执行结果:result的值为0
【3】分析:导致共享变量在线程间不可见的原因
【4】 安全代码:
【5】synchronized 实现内存可见性的解决方案:
重点:为啥synchronized原子性可以避免线程交叉执行:因为synchronized加锁在对象上,执行read方法的线程1获得了对象锁,那线程2不能获得对象锁也就不能执行write方法,因为要执行write方法需要获得锁。但是线程1可以继续执行write方法,因为write方法和read方法可以使用同一把锁,synchronized锁可以重入
4、synchronized实现可见性结果分析:
此处执行结果为6的情况进行分析:
1 synchronized完美保证共享变量的可见性
2 但是不加此关键字,并不意味着就不能实现可见性
【1】为何不加synchronized也会执行可见性,主内存及时更新被获取最新值”?
原因有很多个
①即使没有加synchronized,也可能是可见的,在大多数情况都是可见的,因为编译器优化了,会揣摩程序的意图,程序运行很多次,只会有很少的情况不可见。
②因为当时定义说加synchronized一定会可见性,而不加也没说一定不会,只是有可能不会,因为现在Java做了一些优化:尽量实现可见性;但是不能保证每次都成功,只是成功概率比较大99%,但还是有1%的情况会失败。所以处于安全考虑,尽量加synchronized关键字100%成功。
【2】有时候依然不存在线程交叉情况,但还是会先执行第二个线程,因为第一个线程把CPU让位出来,所以为了避免这种情况,可以在第一个线程后附上代码:sleep(1000);1秒之后才有机会执行线程2。
【3】synchronized+sleep();黄金搭档。
三、volatile实现可见性
1、volatile能够保证可见性
volatile关键字:
volatile 如何实现内存可见性:
深入来说:通过加入内存屏障和禁止重排序优化来实现的。
- 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
- 对volatile变量执行读操作时,会在读操作前后加入一条load屏障指令
执行引擎对 volatile 的操作:
线程 读、 写 volatile 变量的过程
【1】线程写volatile变量的过程:
【2】线程读volatile变量的过程:
volatile不能保证volatile变量复合操作的原子性:
public class VolatileDemo {
private Lock lock = new ReentrantLock();
private int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock();
try {
this.number++;
} finally {
lock.unlock();
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
注意:
理论来讲,最后的值应该是500,但是因为num++;不是原子操作,且volatile关键字又没有原子性,所以偶尔会出现<500的情况。
2、程序分析
num++不是原子操作,原子操作意为(所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch --百科),volatile能保证可见性,但是在多线程调度时 num++ 被拆分为
基于 number = 5 的分析:
安全性解决方案:
保证number自增操作的原子性:
3、保证 number 变量在线程中的原子性
【1】用synchronized 保证 number 变量在线程中的原子性
public synchronized void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.number++;
}
【2】用ReentrantLock 实现number 变量在线程中的原子性
private Lock lock = new ReentrantLock();
private int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock();
try {
this.number++;
} finally {
lock.unlock();
}
}
4、volatile适用场合
要在多线程中安全的使用volatile变量,必须同时满足:
a)对变量的写入操作不依赖其当前值
b)该变量没有包含在具有其他变量的不变式中
5、synchronized和volatile比较
补充:
【1】对于64位(long、double)变量的读写可能不是原子操作:
.Java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的读写操作来进行
导致问题:有可能会出现读取到"半个变量"的情况
解决方法:加volatile关键字
【2】问:即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存见得到及时的更新?
答:一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快滴刷新缓存,所以一般情况下很难看到这种问题.