欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

并发编程CAS与Volatile

程序员文章站 2022-05-04 12:52:25
...

第一、并发编程三大特性
原子性:指一系列操作是不可分割的,一旦执行则整个过程将会一次性全部执行完成,不会停留在中间状态。(有点类似于事务的概念)。
举例:例如A向B汇款1000元,那么就需要有两个操作,一个是A账户减1000元,另一个是B账户增加1000元,如果这个过程中任何一个操作出现故障,都是不符合规矩的也是不能保障汇款人和收款人的财产安全。换句话说,如果想要保证每次转账都不会造成双方任何一方的财产损失,我们必须要保证操作的原子性。要么都做,要么都不做。

可见性:多个线程访问同一共享数据的时候,如果某一个线程修改了此共享数据,那么其他线程能够立即看到此数据的改变。即修改可见。

有序性:代码执行时的顺序与语句顺序一致。也就是说执行前不重排。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
第二、CAS
定义一个账户接口:Account .java

import java.util.ArrayList;
import java.util.List;
/**
 * 定义一个账户接口
 * @author shixiangcheng
 * 2019-12-17
 */
public interface Account {
	//获取余额
	Integer getBalance();
	//取款
	void withdraw(Integer amount);
	//方法内启动1000个线程,每个线程做-10元的操作。若初始余额为10000,那么正确结果应当为0
	static void demo(Account account) {
		List<Thread> ts=new ArrayList<Thread> ();
		long start=System.currentTimeMillis();
		for(int i=0;i<1000;i++) {
			ts.add(new Thread(()->{
				account.withdraw(10);
			}));
		}
		ts.forEach(Thread::start);
		ts.forEach(t->{
			try {
				t.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		long end=System.currentTimeMillis();
		System.out.println(account.getBalance()+" cost: "+(end-start)+"ms");
	}
}

2.接口实现类AccountImpl.java

import java.util.concurrent.atomic.AtomicInteger;
/**
 * 接口实现类
 * @author shixiangcheng
 * 2019-12-17
 */
public class AccountImpl implements Account {
	private AtomicInteger balance;//账户余额
	public AccountImpl(int balance) {//通过构造方法给一个默认的余额
		this.balance = new AtomicInteger(balance);
	}
	@Override
	public Integer getBalance() {
		return balance.get();
	}
	@Override
	public void withdraw(Integer amount) {
		//不断尝试,直到成功为止
		while(true) {
			int prev=balance.get();
			int next=prev-amount;
			/**比较交换:在set前先比较prev和当前值?
			 * 不一致,next作废,cas返回false标识失败
			 * 一致,以next设置为新值,返回true标识成功
			 */
			if(balance.compareAndSet(prev, next)) {
				break;
			}
		}
	}
}

测试类Test.java

/**
 * 测试类
 * @author shixiangcheng
 * 2019-12-17
 */
public class Test {
	public static void main(String [] args) {
		Account.demo(new AccountImpl(10000));
	}
}

测试结果

0 cost: 131ms

总结:compareAndSet的简称就是CAS,它必须是原子操作。CAS的底层是lock cmpxchg指令(X86架构),在单核CPU和多核CPU下都能够保证比较-交换的原子性。在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线,这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
第三、Volatile

/**
 * 测试可见性
 * @author shixiangcheng
 * 2019-12-17
 */
public class TestVolatile{
	static boolean run=true;
	static long i=0;
	public static void main(String [] args) throws InterruptedException {
		System.out.println("A");
		Thread t=new Thread(()-> {
			System.out.println("1");
			while(run) {
				i++;
			}
			System.out.println("2");
		});
		t.start();
		Thread.sleep(1000);
		run=false;
		System.out.println("B");
	}
}

阅读代码推测,程序执行结果输出:A 1 B 2,但是实际执行后执行结果:
并发编程CAS与Volatile
从执行结果看,右上角还有一个红色的标识,标识代码没有执行结束。t线程没有结束,而是一直在while中循环,因为其并没有感知到run的值已经被修改了。也就是主线程对共享变量的修改,对其它线程不可见。这将会造成线程不安全。
java线程内存模型如下:

并发编程CAS与Volatile
每个线程有独立的工作内存(寄存器和高速缓存合称工作内存),为提高执行效率,线程会将数据从主存复制一份到工作内存,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。对于jvm来说,主内存是所有线程共享的java堆,而工作内存中的共享变量的副本是从主内存拷贝过去的,是线程私有的局部变量,位于java栈中。
解决方案:

static volatile boolean run=true;

在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),
内存屏障会提供3个功能:
1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2.它会强制将对缓存的修改操作立即写入主存;
3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

第四、CAS的特点
结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CPU的场景下。
CAS是基于乐观锁的思想。
synchronized是基于悲观锁的思想。
CAS体现的是无锁并发,无阻塞并发
因为没有synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受到影响。
第五、为什么无锁效率高
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。 但无锁情况下,因为线程要保持运行,需要额外CPU支持,如果没有,线程高速运行也就无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

注意:使用CAS时,线程数不可以超过CPU的核心数。

欢迎大家积极留言交流学习心得。