高并发编程:15、缓存一致性,volatile关键字(一)

一、概述

  • cpu缓存一致性。
  • java线程缓存一致性。
  • volatile关键字:可见性。

二、缓存一致性

1、先看一段代码

public class Thread17 {

	private static Integer INIT_VALUE = 0;
	private static Integer MAX_LIMIT = 50000;
	
	public static void main(String[] args) {
		test1();
	}
	
	public static void test1() {
		new Thread(new Runnable() {
			public void run() {
				while(INIT_VALUE < MAX_LIMIT) {
					if(0 != INIT_VALUE) {
						System.out.println("read : " + INIT_VALUE);
					}
				}
			}
		},"read").start();
		
		try {
			Thread.sleep(1*1000);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}
		
		new Thread(new Runnable() {
			public void run() {
				while (INIT_VALUE < MAX_LIMIT) {
					if(INIT_VALUE == 0) {
						System.out.println("update : start");
					}
					++INIT_VALUE;
				}
				System.out.println("update : stop");
			}
		},"update").start();
	}
	
}
update : start
update : stop

可以看到创建了两个线程:一个读取 INIT_VALUE 的线程,一个改变 INIT_VALUE 的修改线程。

在读线程中当INIT_VALUE != 0 时输出日志。

修改INIT_VALUE值的线程,在修改INIT_VALUE为 ++INIT_VALUE 后,读线程并没有输出日志。

需要理解这个问题,先来了解一下多cpu或单cpu多核情况下的缓存一致性问题:

  • 首先我们得知道什么是缓存,并明白什么是缓存,缓存是介于物理存储与CPU处理之间的一段内存空间,主要用于存储从物理存储读出、或者要写入的数据,这需要硬件或者软件支持。如果读取或写入物理存储中的一个字节或一段数据,如果没有缓存,那么每次的读写请求都会直接访问物理存储,而物理存储的速度一般都比较慢,而且物理定位也比较慢,缓存使用后,可以一次性读出需要的数据相邻的数据,暂时存储在缓存中,下面如果还要读取,而这部分数据已经在缓存了,就不需要再去读取物理存储,同样,如果是写操作,可以先将需要写入的数据暂时保存在缓存中,等到缓存过期或者强行清空时,再一次写入物理存储。这样可以把多次的物理存储访问,变成一次物理存储的访问,提高访问效率。
  • 缓存的一致性就是指缓存中的数据是否和目标存储中的数据是一样的,也就是说缓存中已经修改得数据是否已经保存到了物理存储中,物理存储中已经被修改得内容,是否与缓存的内容是一样的。这就是一致性的概念。而多线程中出现的缓存一致性问题我们用一个例子来说明:
 i = i + 1;
  • 当线程执行这个语句时,会先从主存(我们开发中常用到的RAM内存)当中读取i的值,然后复制一份到高速缓存(在CPU中的cache)当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,也存在该问题,稍后会说)。
  • 比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
  • 可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。虽然执行了两次对i的操作,但是输出却只加了1。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

既然出现了缓存一致性的问题,目前通常用在硬件层面解决有两种方法:

是通过在总线加lock#锁的方式。

是通过缓存一致性协议。

  • 在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
  • 但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取,这样就避免了出现缓存一致性问题。

   

2、在了解了cpu之间的缓存一致性问题之后,现在回过头来看看java多的内存模型 :

   

  • 可以看到java的内存模型中的工作内存跟cpu的cache很像:每个线程有自己私有的工作内存区,JVM会复制对象到线程的工作内存区,其他线程不可见。当线程需要修改主存中的对象时,先操作线程私有的工作内存区,操作完成后再把对象写回去主存,这就会出现问题。Java提供了volatile关键字保证了内存的可见性(volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存),底层通过LOCK#或“缓存锁定”实现。如
private static volatile Integer INIT_VALUE = 0;
update : start
read : 737
read : 935
read : 1015
read : 1142
read : 1142
read : 1171
read : 1251
  • 可以看到用volatile修饰INIT_VALUE变量后,修改线程修改INIT_VALUE值的时候,读线程能够看到INIT_VALUE值的改变。

3、来思考一个问题,按照上面的理解当不添加volatile关键字的时候,读取线程读取不到最新的INIT_VALUE值,那么如果读线程也改为和修改线程一样的逻辑,那是否两个线程都会各自打印出来0~50000的数值呢?

public class Thread17 {

	private static Integer INIT_VALUE = 0;
	private static Integer MAX_LIMIT = 50000;
	
	public static void main(String[] args) {
		test2();
	}
	public static void test2() {
		new Thread(new Runnable() {
			public void run() {
				while(INIT_VALUE < MAX_LIMIT) {
					++INIT_VALUE;
					System.out.println("read : " + INIT_VALUE);
				}
			}
		},"read").start();
		
		try {
			Thread.sleep(10);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}
		
		new Thread(new Runnable() {
			public void run() {
				while (INIT_VALUE < MAX_LIMIT) {
					++INIT_VALUE;
					System.out.println("update : " + INIT_VALUE);
					
				}
			}
		},"update").start();
	}
}
update : 45370
update : 45371
update : 45372
update : 45373
update : 45374
update : 45375
update : 45376
update : 45377
update : 45378
read : 45300
read : 45380
read : 45381
read : 45382
read : 45383
read : 45384
read : 45385
read : 45386
update : 45379
update : 45388
update : 45389
update : 45390
update : 45391
  • 可以看到并不会各自打印0~50000数值,而是在读线程中也读取到了INIT_VALUE变化的值,这是因为JVM给程序作了优化,当线程读取某一个变量,不做修改的时候,第一次访问主内存的数据时,会存储在工作空间中,后续直接使用cache,不会在从主内存中获取新数据,需添加volatile关键字来保证变量的可见性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值