java 无锁实现原理CAS解析

以一段取款余额引出问题

  • 账户余额提取问题
public interface Account {

    public static void main(String[] args) {
        // 不安全 无锁
        Account accountUnsafe = new AccountUnsafe(10000);
        Account.demo(accountUnsafe);

    }
    

    // 获取余额 
    Integer getBalance();
    

    // 取款
    void withdraw(Integer amount);
    
    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        //创建1000个线程
        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.nanoTime();
        System.out.println(account.getBalance()
                + " cost: " + (end-start)/1000_000 + " ms");
    }
}




  • 实现一不安全
class AccountUnsafe implements Account{

        //余额
        private Integer balance;

        public AccountUnsafe(Integer balance) {
            this.balance = balance;
        }

        @Override
        public Integer getBalance() {
            return this.balance;
        }

        @Override
        public void withdraw(Integer amount) {
          this.balance -= amount;
        }
    }
    
    //执行
    public static void main(String[] args) {
        // 不安全 无锁
       Account accountUnsafe = new AccountUnsafe(10000);
       Account.demo(accountUnsafe);

    }
    
    结果: 250 cost: 197 ms

结论:1000个线程对账号为10000余额扣减10,结果应为0。因为出现了共享资源的竞争问题,多线程导致结果出错。

解决方式-加锁

 class AccountSynchronized implements Account{

        //余额
        private Integer balance;

        public AccountSynchronized(Integer balance) {
            this.balance = balance;
        }

        @Override
        public Integer getBalance() {
            return this.balance;
        }

        @Override
        public void withdraw(Integer amount) {
           synchronized (this){
               this.balance -= amount;
           }
        }
    }
    //执行
     public static void main(String[] args) {
   
       // synchronize 加锁
        Account accountSynchronize = new AccountSynchronized(10000);
        Account.demo(accountSynchronize);

    }
    结果: 0 cost: 209 ms

结论:结果正确

解决方式-无锁

 class AccountCas implements Account{

        //余额
        private AtomicInteger balance;

        public AccountCas(Integer balance) {
            this.balance = new AtomicInteger(balance);
        }

        @Override
        public Integer getBalance() {
            return balance.get();
        }

        @Override
        public void withdraw(Integer amount) {
            //CPU 指令级别 原子操作不可分割
           while(true){
               int prve = balance.get();
               int next = prve - amount;
               // CAS 比较并设置 机制:会以prve与当前最新的balance值作比较,如果过相同则将值设置为next
               // 若失败则不断进行尝试
               if(balance.compareAndSet(prve,next)){
                   break;
               }
           }
        }
    }
    //执行
     public static void main(String[] args) {
   
       // CAS 无锁
        Account accountCas = new AccountCas(10000);
        Account.demo(accountCas);

    }
    结果: 0 cost: 279 ms

结论:结果正确


Cas与volatile

  • CAS
 public void withdraw(Integer amount) {
            // 需要不断尝试,直到成功为止
           while(true){
               //获取旧值100
               int prve = balance.get();
               // next = 100 - 10 = 90
               int next = prve - amount;
               // CAS 比较并设置 机制:会以prve与当前最新的balance值作比较,如果过相同则将值设置为next
               // compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
               //不一致了,next 作废,返回 false 表示失败
               //比如,别的线程已经做了减法,当前值已经被减成了 90
               //那么本线程的这次 90 就作废了,进入 while 下次循环重试
               //一致,以 next 设置为新值,返回 true 表示成功
               if(balance.compareAndSet(prve,next)){
                   break;
               }
           }
        }

其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。

线程一Account账户线程二获取余额100减10 = 90修改为90cas(100,90)失败获取余额90修改为80减10 = 80cas(90,80)失败减10 = 70cas(80,70)成功线程一Account账户线程二

注意 cas底层是在cpu指令上lock cmpxchg,在单核cpu与多核cpu都能保证【比较-交换】的原子性。
在多核的cpu下,某一个核执行带lock的指令,CPU会让总线锁住,当这个核把指令执行完毕,在开启总线。这个过程中指令的执行不会被线程调度机制锁打断,保证多线程对内存操作的原子性。

  • volatile

获取共享变量时,为了保持可见性需要使用volatile

volatile可以修饰成员变量与静态成员变量,防止变量从工作缓存中获取变量,必须从主存中获取变量,线程操作volatile变量直接操作主存,即线程对volatile修改对另一个线程可见

注意: volatile只能解决线程的可见性问题,不能解决指令交错问题(不能保证原子性)

Cas必须使用volatile变量 保证共享变量的可见性,才能实现【比较与交换】

以AtomicInteger为例:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // 内部维护了value 被volatile所修饰
    private volatile int value;

无锁效率相对高

  • 在无锁的状态下,即使重试失败,线程始终处于高速的运行状态下,而使用sychronied会让线程在没有锁的情况下,上下文切换,进入阻塞。
  • 举个例子:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,
    等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
  • 在无锁的情况,保持线程的运行,需要额外的CPU支持。CPU相当于跑道,线程的运行无从谈起,虽然不会进入阻塞,但是由于没分到时间片,线程处于可运行状态,但是依然会导致线程的上下文切换

CAS的特点

结合CAS与volatile的特点,无锁使用与线程数少,CPU核数多的情况

  • CAS无锁为基于乐观锁思想:乐观的估计,不怕其他线程修改共享变量,就算改了,在进行重试
  • Synchronized基于悲观锁思想:悲观的估计,防止其他线程修改共享变量,修改完成后在解锁。
  • CAS无锁并发,无阻塞并发:
    • 因为不需要进行线程的上下文切换,所以效率很高
    • 但是在竞争激烈的情况,频繁的重试,反而影响效率
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章