MESI协议和Volatile关键字(解决多线程下变量更新不同步问题)

多线程下变量更新不同步问题

测试一个通过修改while循环中标志位来结束一个线程的方法。使while循环中的内容为空,启动线程,线程死循环。当我们在主线程里把标志位flag置为false时,发现线程没有结束(即循环没有退出)。代码如下:

public class Test {
        boolean flag=true;
	    public static void main(String[] args){
	    	Test test = new Test();
	    	test.demo();
	    }	
	    public void demo(){
	    	Thread t1 = new Thread(){	    		
		    		public void run()  //重写run方法
		    		{
		    			while(flag==true)  
		    			{			    		
		    			}
		    			System.out.println("线程结束了。。。。。。。。");
		    		}		
	    	};	    	
	    	t1.start();
	    	try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
            flag=false;
	    }	
}

理论上在修改flag后线程会离开循环并输出结束语句,但是没有。当我在while循环中加上一些执行内容后,便可以正常退出。为什么会有这样的现象发生并且如何去解决这个问题呢?
答案是:只要在变量的定义处加上volatile关键字就可以实现变量更新的同步。

MESI协议和volatile关键字

CPU的运算速度是远远大于CPU和主存之间的数据读写速度的,CPU中存在高速缓存(可以有多级),CPU可以和高速缓存之间进行高速的数据读写操作。比如要执行i=i+1这个语句,CPU会先向主存中读取i变量的数值,存入自己的高速缓存中,然后对i变量进行加一操作,写到高速缓存,最后再刷新到主存中。当对运算的要求比较高的时候,CPU会对高速缓存中的数据进行一系列操作后统一刷回主存,这就可以解释我们上面看到的现象了,为什么CPU中flag值没有被更新为false,在线程读取flag=true值到高速缓存中后,就在while循环中不断轮询这个变量值,因为我们这次没有加上打印语句(打印语句的执行时间是比较长的),为了保证读取的高效,CPU保持和其高速缓存进行交互,而没有重新去主存中读取新值(虽然主存中的值已经被修改了),循环就一直持续下去。
CPU和主存交互示意图:
在这里插入图片描述
我们在变量的定义处加上了volatile关键字,解决了这个问题,线程可以被结束。如:volatile boolean flag=true;
为什么呢?我们继续上面关于CPU和主存的讨论。多CPU单主存以及多线程并发访问主存会导致变量的不同步,这里我们称它为缓存一致性问题,只有解决了这个问题,才能实现在多线程并发访问的时候高效无误。
解决缓存一致性问题的方法:
一种方案是对总线加锁,使得每一时刻只能有一个CPU对总线进行访问,确保它对数据操作完之后再释放锁,允许别的CPU通过总线访问主存,这种方案实现了操作的原子性,但它会使得当一个CPU运作时别的CPU处于闲置状态,效率很低。现在广泛采用的方案是Intel的MESI协议,解决了缓存一致性问题并实现了高效。
MESI协议:
1、M(修改):当某CPU中的缓存条被修改后(和主存中的内容不一致),其他CPU中的相应缓存条会失效,如果有别的CPU需要访问该缓存条中的内容,则需要此修改后的缓存条把数据刷新到主存,然后把模式切换到共享(S)。
2、E(独享):当某CPU中缓存条的内容和主存中的一致并且独享该缓存条的时候,有别的CPU需要访问该资源,那么只要把模式切换到共享(S)就可以了。
3、S(共享):在这个模式下,任意CPU都可以访问此模式的缓存条中的数据,之后切换到相应模式就可以了。
4、I(无效):此状态下的缓存条无效。
MESI协议规定了缓存条的状态,从而实现了缓存一致性原则,在多CPU及多线程的访问下,保证了数据的同步。
而volatile关键字就是对于这个协议的一种应用,加上它之后,在主线程中修改了flag的值,程序就会告诉另一个线程该位置缓存条已经失效,要求其从主存中重新读取flag值,从而退出了循环。
volatile关键字实现了并发编程的两大要素,这里简单介绍一下:
1、可见性:保证在多线程的情况下,某一变量对于多个线程来说都是“可见”的,这里的可见指的是当一个线程对变量修改之后,别的线程可以立刻知道,这实现了变量的同步。
2、有序性:我们一般会认为程序的执行是逐上至下有序执行的,其实不然,由于java代码功能不同(处理的寄存器不同),为了提高效率,java会把具有相同功能的指令归类起来执行,减少不同类别指令之间的切换,从而提高流水线作业的效率,当然,这会保证程序执行的效果和顺序执行下的效果相同,但这仅限于单线程,在多线程中可能会出错,如以下伪代码:
线程1:
content=load();
flag=true;
线程2:
while(flag==false)
Thread.Sleep(100);
do something with content…
在线程1中,两条语句没有直接的逻辑关系,有可能交换顺序执行。但是到了线程2,如果在线程1未加载数据的情况下就把标志位置true引发线程2对数据的操作,就会出错。这时如果在flag的定义处加上volatile关键字,就可以避免重排。因为加了volatile关键字的变量就像是一个屏障,它下面的指令不能到它上面,它上面的指令也不能到它的下面,保证了有序性。
**注:**并发编程另外一大重要的特性是原子性,volatile关键字不能保证。
原子性:每个线程对该变量的操作都是原子的,原子操作是指,某个操作要不不执行,要不就一口气执行完并且保证不会被打断。原子操作我们在低层一般通过synchronize关键字来实现,用锁的机制来确保在执行这段程序的时候不会被干扰,同时这也体现了可见性,因为这是原子操作,执行完别的线程就会知道。volatile在执行类似:i++这样的指令时,需要经历加载变量,修改变量,在主存中更新变量这几步,不是原子操作,中间是可能被打断的。如果把i++放在synchronize关键字修饰的方法体中(加锁),就可以把它变成原子性操作。
说了很多,我们最后再回到最初的例子:volatile关键字实现了MESI协议,保证了多CPU(或多线程)对变量修改符合缓存一致性原则,加上volatile关键字后,确保了多线程对变量修改的可见性,在一处修改变量后,另一处就要重新对变量进行读取,也就实现了线程的退出。

猜你喜欢

转载自blog.csdn.net/mayifan_blog/article/details/85538261
今日推荐