并发编程CAS与Volatile
第一、并发编程三大特性
原子性:指一系列操作是不可分割的,一旦执行则整个过程将会一次性全部执行完成,不会停留在中间状态。(有点类似于事务的概念)。
举例:例如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,但是实际执行后执行结果:
从执行结果看,右上角还有一个红色的标识,标识代码没有执行结束。t线程没有结束,而是一直在while中循环,因为其并没有感知到run的值已经被修改了。也就是主线程对共享变量的修改,对其它线程不可见。这将会造成线程不安全。
java线程内存模型如下:
每个线程有独立的工作内存(寄存器和高速缓存合称工作内存),为提高执行效率,线程会将数据从主存复制一份到工作内存,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。对于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的核心数。
欢迎大家积极留言交流学习心得。