Java多线程之内存可见性

1.可见性

    1.1 什么是可见性?

        一个线程对共享变量值的修改,能够及时的被其他线程看到。

    1.2 什么是共享变量?

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

        什么是线程的工作内存呢? 这个其实是Java内存模型抽象出来的一个概念

    简单了解一下Java内存模型:

        Java内存模型(JMM): (英文:Java Memory Model

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

        上面所说的变量指的就是前面所说的共享变量,如果不是共享变量,自然就不会牵扯到数据在线程之间的争用,因为不会有数据的争用,那么数据自然而然就是安全的。

        

            1. 所有的变量都保存在主内存中。

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

        Java内存有如下两条规定:

            1. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。

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

综上所诉,可见性的意思如下:

要实现共享变量的可见性,必须保证两点:

    1. 线程修改后的共享变量值能够及时从工作内存中刷新到主内存中。

    2. 其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中。

只有保证这两点,才能保证当前线程对共享变量的修改能够及时被其他线程所看到。

Java语言层面支持的可见性实现方式:

    1. synchronized

    2. volatile


2. synchronized

    synchronized能够实现:

        1. 原子性(同步)

        2. 可见性


JMM关于synchronized的两条规定:

    1. 线程解锁前,必须把共享变量的最新值刷新到主内存中。

    2. 线程加锁前,必须把工作内存的共享变量清空,从而使用共享变量时需要从主内存中重新读取最新的数据

    (注意,加锁和解锁需要是同一把锁

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

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

    1. 获得互斥锁

    2. 清除工作内存

    3. 从主内存拷贝变量的最新副本到工作内存

    4. 执行代码

    5. 将更改后的共享变量的值更新到主内存中

    6. 释放互斥锁


接下来写一段代码进行演示:

public class SynchronizedDemo {
	//共享变量
	private boolean ready = false;
	private int number = 1;
	private int result = 0;
	
	//写操作
	public void write(){
		ready = true;     // 1.1
		result = 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){
				write();
			} else {
				read();
			}
		}
	}
	
	public static void main(String[] args) {
		SynchronizedDemo synDemo = new SynchronizedDemo();
		//程序执行写操作
		synDemo.new ReadWriteThread(true).start();
		//程序执行都操作
		synDemo.new ReadWriteThread(false).start();
	}
}

在这个程序中声明了两个方法,一个是写的操作,给共享变量赋值,一个是读操作,将共享变量的值进行修改。

还声明了一个内部类,通过构造函数传入的boolean来确定是否是读操作还是写操作。

在main方法进行测试。

会有两种结果:


出现6的结果有很多种,可能顺序如下:

1.1 -> 1.2 -> 2.1 -> 2.2

1.2 -> 1.1 -> 2.1 -> 2.2   //进行了重排序

1.1 -> 2.1 -> 1.2 -> 2.2

......... 

但是可能有一点有疑问的地方,那就是我们没有添加synchronized关键字,ready和number它们怎么会在工作内存和主内存进行了更新呢?因为只有更新了才会出现正确的值 6,但是Java内存并没有说如果我们不加synchronized关键字,共享变量就一定不可见了,所以这里没有加synchronized关键字,共享变量也有可能被其他线程看到,事实上没有加synchronized,共享变量依然可以在主内存和工作内存得到及时的更新,这个主要是编译器做了一个优化,它会去“揣摩”程序的“意图”,去尽量给我们一个正确的答案,所以说可能程序可能运行很多次,才不会出现这个结果,但是往往就是因为这一次结果,会造成许多的重大的灾难。


出现0的情况可能如下:

1.2 -> 2.1 -> 2.2 -> 1.1

另外还有可能读线程得到了先执行,再执行写线程

3. volatile

volatile实现可见性:

    1. 能够保证volatile变量的可见性

    2. 不能保证volatitle变量复合操作的原子性

   

volatile如何实现内存可见性:

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

    1. 对于volatitle变量执行写操作时,会在写操作后加入一条store屏障指令。

   这条指令会把CPU中写的缓存,强制加到主内存中去,所以在主内存存放的变量就是最新的值了。

    2. 对于volatile变量执行都操作时,会在都操作之前加入一条load屏障指令。

    这条指令会使缓冲区的缓存失效,所以每次读取volatitle变量的值的时候,就需要从主内存中去读取它执行的值。当然了,它也可以起到禁止重排序的效果。

    (Ps:对于store和load指令,其实在Java内存当中,一共定义了8条操作指令,来完成主内存和工作内存的交互操作,store和load只是其中的两条。)

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

    

如上图所示,一个写操作和一个读操作,结合起来就保证了volatile变量在主内存和工作内存的能够得到及时的更新。这样在每一个时刻,每个线程都能看到volatitle变量的最新值了。


但是volatile跟synchronized的区别在于:

volatile不能保证volatile变量复合操作的原子性:


 number++不是原子性操作,因为它需要经过3个步骤。如上图所示。

但是现在,如果在这行代码加到synchronized代码块中:


 因为synchronized代码块可以保证锁内操作的原子性。

 所以在加入synchronized关键字后,则变为了原子操作。换句话来说,number++只能被一个线程执行完之后,才能被另外一个线程执行,那你不能够同时几个线程同时交叉去执行这3步操作。

如果把number改为了volatile变量呢:


变为volatile变量之后,无法保证原子性。

接下来看一段代码示例:

public class VolatileDemo {
	private volatile int number = 0;
	
	//获取number
	public int getNumber(){
		return this.number;
	}
	
	//number++操作
	public void increase(){
		this.number++;
	}
	
	public static void main(String[] args) {
		//匿名内部类访问外部类局部变量必须为final属性
		final VolatileDemo volatileDemo = new VolatileDemo();
		
		//定义一个匿名内部类
		for(int i = 0 ; i < 500 ; i++){
			new Thread(new Runnable(){
				@Override
				public void run() {
					volatileDemo.increase();
				}
			}).start();
		}
		
		//如果还有子线程在运行,主线程就让出CPU资源
		//直到所有的子线程都运行完了,主线程再继续往下执行
		while(Thread.activeCount() > 1){
			Thread.yield();
		}
		
		//输出Number的值
		//如果无误的话,正确的值为500
		System.out.println(volatileDemo.getNumber());
	}
}

输出的结果如下:




从前面已经说过,number++是分为3步来进行的,先读取number值,再加1,最后再将值写入到内存中去,但是volatile又不能保证这3个操作的原子性,所以说可能会有3个线程来交叉执行这3个步骤。

举个例子来说明一下:

当number = 5 的时候,

1. 线程A读取number的值

假设当线程A读取完number的值之后呢,CPU资源就被抢走了,所以说呢这个时候线程A就会被阻塞住。假设这个时候另外一个线程B它获取到了CPU的执行权。

2. 线程B读取number的值

3. 线程B执行加1操作

4. 线程B写入最新的number的值

这个时候来猜一下,number的值在主内存和线程B的工作内存中是多少?应该都是6。


因为线程B读取完number之后呢,number的值为5,然后执行加1操作,这个时候很明显,B的工作内存中number的值为6,根据volatile变量写的规则呢,写入之后呢,会把最新值存入到主内存中。执行完这三个操作之后,线程A再次获取到CPU的资源。此时此刻中,请问线程A中的工作内存的number值是多少?


因为之前所有的操作都是在线程B下执行的,跟线程A没有任何关系,所以接下来线程A没有任何的操作让主内存的最新值刷新到工作内存当中去。所以线程A工作内存的number值还是5。

5. 线程A执行加1操作。

6. 线程A写入最新的number值。


因此程序就会有很多次,小于500的情况。

那么怎么来解决这个问题呢?

问题的根本就是volatile没有原子性。

所以保证number自增操作的原子性:

1. 使用synchronized关键字

2. 使用JDK1.5之后的ReentrantLock(java.util.concurrent.locks包下)

3. 使用JDK1.5之后的AtomicInteger(java.util.concurrent.atomic包下)


使用synchronized关键字,需要把volatile关键字去掉,再increase()方法再加上synchronized。


第2种情况:


这个加锁和解锁其实就相当于进入和退出synchronzied的代码块。而且它也可以保证number++的可见性和原子性。

加上锁的释放是由于锁内部的操作呢,可能会抛出一些异常。

但是lock锁比synchronized具备其他的功能。


volatile适用场景:


但是在实际情况中,大多数都会跟上面其中一个条件冲突,所以volatile并没有像synchronized运用的那么广泛。


学于慕课网:

传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值