关于volatile关键字及其可见性和非原子性的相关测试

volatile:一种轻量级的同步机制

能够保证:1.内存可见性(通过强制刷新cpu中的数据) ;2.保证有序性;(通过禁止指令重排实现)
不能保证:** 原子性。

关于指令重排:指令重排是指在编译Java代码或者cpu执行字节码时,对字节码指令进行重新排序。
volatile 如何禁止指令重排?(通过cpu指令内存屏障)

  • 1、在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。

  • 2、在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。

    关于内存屏障的补充说明:

  • 1、LoadLoad屏障:
    抽象场景:Load1; LoadLoad; Load2
    Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • 2、StoreStore屏障:
    抽象场景:Store1; StoreStore; Store2
    Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见

  • 3、LoadStore屏障:
    抽象场景:Load1; LoadStore; Store2
    在Store2被写入前,保证Load1要读取的数据被读取完毕。

  • 4 、StoreLoad屏障:
    抽象场景:Store1; StoreLoad; Load2
    在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。

以下式关于内存可见性的代码测试:

public class TestVolatile{
	volatile int num = 0;
	
	public static void main(String[] args) {
		TestAtomic tv = new TestVolatile();
		new Thread() {
			public void run() {
				System.out.println("A com in");
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				tv.num = 1;
				System.out.println("num is changed,num="+tv.num);
			};
		}.start();
		
		while(tv.num==0) {
			//System.out.println(tv.num); 关于这句代码,加上会导致不能测试出加volatile和不加的区别。通过对字节码的
														分析可知,这部分进行了两次从内存中取值,并不是一个原子操作。
		}
		System.out.println("main is finish");
	}
}

关于原子性的代码测试:

public class TestVolatile {
	volatile int num = 0;
	
	public void add() {
		num++;
	}
	
	public static void main(String[] args) {
		TestVolatile tv = new TestVolatile();
		
		for(int i = 0;i<20;i++) {
			new Thread() {
				public void run() {
					for(int j = 0;j<1000;j++) {
						tv.add();
					}
				}
			}.start();
		}
		
		if(Thread.activeCount()>1) {
			System.out.println(tv.num);
		}
	}
}

按照我们所想:20个线程,每个线程执行1000次++操作,最后的结果应该是20000,可是得到的结果却总是小于20000,实际上由于volatile并不能保证++操作的原子性,(关于i++操作的说明,分为三步,每个步骤中间能够被打断,1.内存到寄存器,2.寄存器自增,3.写回内存,关于赋值操作,其实际上是一个原子操作,只有写回内存这一步)。

猜你喜欢

转载自blog.csdn.net/weixin_43695091/article/details/90201054