CAS原子操作的理解

首先理解悲观锁和乐观锁:

乐观锁:

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。


悲观锁:
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。

CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。(有点像自旋锁)

CAS主要是三个操作:

1.获取当前值

2.当前值加一赋给目标值

3.进行CAS操作,成功跳出循环,失败就重复上述操作

import java.util.ArrayList;  
import java.util.List;  
import java.util.concurrent.atomic.AtomicInteger;  
  
public class Counter {  
    private AtomicInteger ai = new AtomicInteger();  
    private int i = 0;  
  
    public static void main(String[] args) {  
        final Counter cas = new Counter();  
        List<Thread> ts = new ArrayList<Thread>();  
        // 添加100个线程  
        for (int j = 0; j < 100; j++) {  
            ts.add(new Thread(new Runnable() {  
                public void run() {  
                    // 执行100次计算,预期结果应该是10000  
                    for (int i = 0; i < 100; i++) {  
                        cas.count();  
                        cas.safeCount();  
                    }  
                }  
            }));  
        }  
        //开始执行  
        for (Thread t : ts) {  
            t.start();  
        }  
        // 等待所有线程执行完成  
        for (Thread t : ts) {  
            try {  
                t.join();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        System.out.println("非线程安全计数结果:"+cas.i);  
        System.out.println("线程安全计数结果:"+cas.ai.get());  
    }  
  
    /** 使用CAS实现线程安全计数器 */  
    private void safeCount() {  
        for (;;) {  
            int i = ai.get();  
            // 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值  if (ai.compareAndSet(i, ++i)) { 
System.out.println(i);//并不是按顺序输出的,但所有的值都会输出来
break;  
            }  
        }  
    }  
  
    /** 非线程安全计数器 */  
    private void count() {  
        i++;  
    }  
}  
//结果:  
非线程安全计数结果:9867  
线程安全计数结果:10000

CAS操作中的三个问题:

1.ABA问题:

例子:

小灰有100元存款,要用一个提款机来提款50元。

由于提款机硬件出现问题,小灰提款操作被提交两次,开启两个线程,两个线程都是获取当前值100元,要更新成50元。

理想情况下是一个线程更新成功一个线程更新失败,只扣一次。

线程1(提款机):获取当前值100元,成功更新成50元

线程2(提款机):获取当前值100元,期望更新为50,BLOCK

线程3(小灰妈):获取当前值50元,期望更新为100元

在线程1和3执行完了以后,compare以后线程2会再次执行,就会执行两次扣钱。

如何解决这个问题?

除了比较期望值以外还要比较变量的版本号,在线程1操作的时候期望值100,版本号a01,在线程3操作完以后,期望值虽然还是变成了100,但是版本号变为了a03,在线程2去操作的时候发现版本号不一致了,就不会执行这个操作了

参考:https://www.sohu.com/a/215510186_465221

2.循环时间开销大的问题:

观察上面的代码,如果在循环的过程中长时间无法成功不能退出,那么会给cpu带来非常大的执行开销

3.只能保证一个共享变量的操作:

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

参考:https://286.iteye.com/blog/2295165

猜你喜欢

转载自www.cnblogs.com/smallJunJun/p/10478881.html