我们在抢红包那篇文章讲到CAS,是java的乐观锁的一种,我们简单介绍下CAS
CAS的底层原理是lock cmpxchg 指令(X86 架构)在单核和多核CPU下都能保证比较和交换的原子性
程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果
程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时,CPU 会执行总线锁定或缓存锁定,将修改的变量写入到主存,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性
CAS的全称表现在java上sun.misc.Unsafe类的方法。调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作
java cas 例子
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Account account = new AccountCas(10000);
Account.demo(account);
}
}
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(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;
// 真正修改
if(balance.compareAndSet(prev, next)) {
break;
}
}
}
}
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List ts = new ArrayList();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance() + " cost: " + (end-start)/1000_000 + " ms");
}
}
在上述代码中,我们设置一个账户余额有10000元,然后设置1000个线程,每个线程转出10元,正确结果这里应该是0,AccountCas类中,我们定义了一个AtomicInteger对象,CAS的底层操作就是如withdraw函数中所示,获取到当前的值和将要修改的值,然后做一个比较并设置的操作,将Account对象的最新值和当前线程拿到的当前值做对比,如果相等,则返回true,是没有问题的,如果不相等,那么获取当前值,在while(true)中再次执行一次流程。
我们看AtomicInteger的源码,如截图所示
我们给int值加了volatile修饰,才能保证值的原子性操作
volatile 指令重排序
volatile 防止指令重排序,确保操作的顺序性。
volatile 确保变量的修改对所有线程立即可见,避免了线程读取到过期数据的问题。
结合使用 volatile 和 CAS,可以在保证线程安全的前提下,避免使用锁,从而提高并发性能。
我们看下面的例子:
//代码1
package com.test;
import java.util.concurrent.TimeUnit;
public class ThreadDemo extends Thread {
//声明一个变量
private boolean result = false;
public boolean getResult(){
return result;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在线程运行时改变其变量值
result = true;
System.out.println(result);
}
}
//代码2
package com.test;
public class VolatileDemoOne {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
while (true){
if(threadDemo.getResult()){
System.out.println(“11111111”);
}
}
}
}
正常情况下result在线程中已被修改为true,此时控制台将会一直打印11111111,但是此时却没有打印我们预期的结果.这就牵扯到我们的JMM内存模型
计算机模型
在我们现在多核CPU的情况下,多个CPU对我们主内存数据进行读写操作,为了提高其运行速率,将各个cpu读取到的数据存储到与CPU相对应的高速缓存中,这样就实现了各个CPU在自己的高速缓存中对数据进项操作,大大提高了运行效率,为了防止两个高速缓存中的数据不一致问题,计算机采取缓存一致性协议来保证每个CPU对应的高速缓存中的数据与主内存一致。如下图所示:
JMM内存模型
我们可以由图得知,JMM内存模型与我们计算机模型非常相似,那么是如何出现上述原因的呢?
首先当我们启动两个线程去获取变量数据时,都会去主内存中获取数据,并写入到自己的本地内存中,而ThreadDemo线程此时将自己的本地内存中的result改为false,并将数据同步到主内存中,但是此时我们的main线程还是在操作自己工作空间中的老数据,main线程ThreadDemo线程之间不能相互读取各自工作内存中的数据,而main线程读取到的result为false,出现我们控制台的结果
我们把上面的代码 private boolean result = false;修改为private volatied boolean result = false;再次执行代码 就能得到我们预期的结果了
我们加上这个修饰后,result 的值放在主内存中而且不是原来线程的本地缓存中读取。所以达到了我们预期的效果。关于禁止指令重排序的内容,有兴趣的可以去查找相关的资料,这里不再熬述。
CAS和Synchronized
我们在前面的博客中提到Synchronized属于悲观锁,在一个线程未执行完方法的时候,其他线程不能获取到资源并执行。CAS使用比较并交换的方式在未加锁的情况下,实现线程安全的操作。
CAS的ABA问题
我们看完上面的源码应该知道CAS是通过比较并交换的方式达到锁操作的效果,如果在并发量非常高的情况下,不建议使用。因为不断的有线程去修改值,原来的线程又要比较,不通过只能重新执行。在并发非常高的场景使用反而会让性能急剧的下降,而且大量的消耗资源。
标签:JAVA,CAS,CPU,线程,result,public,内存 From: https://blog.csdn.net/yusongcao/article/details/144899712