java并发编程(十三)之线程Volatile内存可见性

时间:2021-11-24 20:50:59

一、内存可见性

Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互。 具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。

package com.dason.juc;

/*
 * 一、volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。
 * 					  相较于 synchronized 是一种较为轻量级的同步策略。
 * 
 * 注意:
 * 1. volatile 不具备“互斥性”
 * 2. volatile 不能保证变量的“原子性”
 */
public class TestVolatile {
	
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		new Thread(td).start();
		
		while(true){
			if(td.isFlag()){
				System.out.println("------------------");
				break;
			}
		}
		
	}

}

class ThreadDemo implements Runnable {
	//保证了内存可见性
	private volatile boolean flag = false;

	@Override
	public void run() {
		
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}

		flag = true;
		
		System.out.println("flag=" + isFlag());

	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}

}

二、缺陷

Volatile虽然保证了线程中变量内存的可见性,但是他不能实现线程之间的互斥性以及变量的原子性。互斥性的实现通常我们采用的是同步的方式(其实同步Synchronized就已经保证了这三个特性,因为它拒绝同一时刻多个线程对同一资源的访问,Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。)。那么什么是原子性呢?即不可再分割,不可被中断。

在 java.util.concurrent.atomic 包下提供了一些原子变量。使用Volatile保证其内存可见性,采用CAS算法保证数据变量的原子性。其中以Atomic开头的包装类。例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。

package com.dason.juc;

import java.util.concurrent.atomic.AtomicInteger;

/*
 * 一、i++ 的原子性问题:i++ 的操作实际上分为三个步骤“读-改-写”
 * 		  int i = 10;
 * 		  i = i++; //10
 * 
 * 		  int temp = i;
 * 		  i = i + 1;
 * 		  i = temp;
 * 
 * 二、原子变量:在 java.util.concurrent.atomic 包下提供了一些原子变量。
 * 		1. volatile 保证内存可见性
 * 		2. CAS(Compare-And-Swap) 算法保证数据变量的原子性
 * 			CAS 算法是硬件对于并发操作的支持
 * 			CAS 包含了三个操作数:
 * 			①内存值  V
 * 			②预估值  A
 * 			③更新值  B
 * 			当且仅当 V == A 时, V = B; 否则,不会执行任何操作。
 */
public class TestAtomicDemo {

	public static void main(String[] args) {
		AtomicDemo ad = new AtomicDemo();
		
		for (int i = 0; i < 10; i++) {
			new Thread(ad).start();
		}
	}
	
}

class AtomicDemo implements Runnable{
	
	//不保证变量的原子性
//	private volatile int serialNumber = 0;
	
	private AtomicInteger serialNumber = new AtomicInteger(0);

	@Override
	public void run() {
		
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}
		
		System.out.println(getSerialNumber());
	}
	
	public int getSerialNumber(){
//		return serialNumber++;
		return serialNumber.getAndIncrement();
	}
	
	
}

那么问题又来了,什么又是CAS算法呢?

三、CAS算法

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值B,要修改的新值B。

当且仅当 V == A 时, V = B; 否则,不会执行任何操作。

举例:

1.在内存地址V当中,存储着值为10的变量。
2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
线程1:A = 10   B = 11
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
线程2:V = 11
4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
线程1:A = 10   B = 11(此时 V = 11)
A != V 失败
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋
A = 11 B = 12
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
A == V 
7.线程1进行SWAP,把地址V的值替换为B,也就是12。

V = B

package com.dason.juc;

/*
 * 模拟 CAS 算法(没有实现自旋)
 */
public class TestCompareAndSwap {
	
	public static void main(String[] args) {
		final CompareAndSwap cas = new CompareAndSwap();
		
		/**
		 * 可能成功,可能失败。
		 * 失败的时候,第一个线程在比较的时候,另一个线程就将其修改了
		 */
		for (int i = 0; i < 10; i++) {
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					int expectedValue = cas.get();
					boolean b = cas.compareAndSet(expectedValue, (int)(Math.random() * 101));
					System.out.println(b);
				}
			}).start();
		}
		
	}
	
}

class CompareAndSwap{
	private int value;
	
	//获取内存值
	public synchronized int get(){
		return value;
	}
	
	//比较 无论成功与否都返回旧值
	public synchronized int compareAndSwap(int expectedValue, int newValue){
		int oldValue = value;//1.拿到内存地址V
		
		if(oldValue == expectedValue){//2.比较预期值与内存值
			this.value = newValue;//3.将内存值替换为新值
		}
		
		return oldValue;
	}
	
	//设置: 成功就替换并返回true,失败返回false
	public synchronized boolean compareAndSet(int expectedValue, int newValue){
		return expectedValue == compareAndSwap(expectedValue, newValue);
	}
}
CAS的缺点:
1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
3.ABA问题
这是CAS机制最大的问题所在。
什么是ABA问题?怎么解决?请参见 ABA问题