欢迎关注专栏【JAVA并发】

前言

日子中咱们看待一个事物总有不同的态度,比如半瓶水,失望的人会觉得只要半瓶水了,而达观的人则会以为还有半瓶水呢。许多技术思维往往源于日子,因此在多个线程并发拜访数据的时分,有了失望锁和达观锁。

  • 失望锁以为这个数据肯定会被其他线程给修正了,那我就给它上锁,只能自己拜访,要等我拜访完,其他人才能拜访,我上锁、解锁都得花费我时刻。
  • 达观锁以为这个数据不会被修正,我就直接拜访,当我发现数据真的修正了,那我也“礼貌的”让自己拜访失利。

失望锁和达观锁其实实质都是一种思维,在JAVA中对于失望锁的完成咱们可能都很了解,能够经过synchronizedReentrantLock加锁完成,本文不打开讲解了。那么达观锁在JAVA中是怎么完成的呢?底层的完成机制又是什么呢?

问题引进

咱们用一个账户取钱的比如来说明达观锁和失望锁的问题。

public class AccountUnsafe {
     // 余额
     private Integer balance;
     public AccountUnsafe(Integer balance) {
     	this.balance = balance;
     }
    @Override
     public Integer getBalance() {
     	return balance;
     }
     @Override
     public void withdraw(Integer amount) {
     	balance -= amount;
     }
}
  • 账户类,withdraw()办法是取钱办法。
public static void main(String[] args) {
        // 账户10000元
        AccountUnsafe account = new AccountUnsafe(10000);
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        // 1000个线程,每次取10元
        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();
        // 打印账户余额和花费时刻
        log.info("账户余额:{}, 花费时刻: {}", account.getBalance(), (end-start)/1000_000 + " ms");
    }
  • 账户默许有10000元,1000个线程取钱,每次取10元,终究账户应该还有多少钱呢?

运转成果:

乐观锁思想在JAVA中的实现——CAS

  • 运转成果显示余额还有150元,显然呈现并发问题

原因剖析:

原因也很简单,取钱办法withdraw()的操作balance -= amount;看着就一行代码,实际上会生成多条指令,如下图所示:

乐观锁思想在JAVA中的实现——CAS

多个线程运转的时分会进行线程切换,导致这个操作不是原子性,所以不是线程安全的。

失望锁处理

最简单的办法,我想咱们都能想到吧,给withdraw()办法加锁,确保同一时刻只要一个线程能够履行这个办法,确保了原子性。

乐观锁思想在JAVA中的实现——CAS

  • 经过synchronized要害字加锁。

运转成果:

乐观锁思想在JAVA中的实现——CAS

  • 运转成果正常,但是花费时刻稍微多了一点

达观锁处理

要害来了,假如用达观锁的思维在JAVA中该怎么完成呢?

大致思路便是我默许不加任何锁,我先把余额减掉10元,终究更新余额的时分,发现余额和我一开始不一样了,我就丢弃当前更新操作,从头读取余额的值,直到更新成功。

找啊找,终究发现JDK中的Unsafe办法提供了这样的办法compareAndSwapInt

乐观锁思想在JAVA中的实现——CAS

  • 先获取老的余额oldBalance,计算出新的余额newBalance
  • 调用 unsafe.compareAndSwapInt()办法,假如内存中余额特点的偏移量BALANCE_OFFSET对应的值等于老的余额,说明的确没有被其他线程拜访修正正,我就斗胆的更新为newBalance,退出办法
  • 否则的话,我就要进入下一次循环,从头获取余额计算。

那么是怎么获取unsafe呢?

乐观锁思想在JAVA中的实现——CAS

  • 静态办法中经过反射的办法获取,由于Unsafe类太底层了,它一般不主张程序员直接运用。

这个Unsafe类的称号并不是说线程不安全的意思,只是这个类太底层了,不要乱用,对程序员来说不大安全。

终究别忘了余额balance要加volatile修饰。

乐观锁思想在JAVA中的实现——CAS

  • 主要为了确保可见性,让线程能够获取到其他线程修正的成果。

运转成果:

乐观锁思想在JAVA中的实现——CAS

  • 余额也为0,正常,而且运转速度稍微快了一丢丢

完成代码:

@Slf4j(topic = "a.AccountCAS")
public class AccountCAS {
    // 余额
    private volatile int balance;
    // Unsafe对象
    static final Unsafe unsafe;
    // balance 字段的偏移量
    static final long BALANCE_OFFSET;
    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
            // balance 特点在 AccountCAS 对象中的偏移量,用于 Unsafe 直接拜访该特点
            BALANCE_OFFSET = unsafe.objectFieldOffset(AccountCAS.class.getDeclaredField("balance"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }
    public AccountCAS(Integer balance) {
        this.balance = balance;
    }
    public int getBalance() {
        return balance;
    }
    public void withdraw(Integer amount) {
        // 自旋
        while (true) {
            // 获取老的余额
            int oldBalance = balance;
            // 获取新的余额
            int newBalance = oldBalance - amount;
            // 更新余额,BALANCE_OFFSET表明balance特点的偏移量, 回来true表明更新成功, false更新失利,继续更新
            if(unsafe.compareAndSwapInt(this, BALANCE_OFFSET, oldBalance, newBalance)) {
                return;
            }
        }
    }
    public static void main(String[] args) {
        // 账户10000元
        AccountCAS account = new AccountCAS(10000);
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        // 1000个线程,每次取10元
        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();
        // 打印账户余额和花费时刻
        log.info("账户余额:{}, 花费时刻: {}", account.getBalance(), (end-start)/1000_000 + " ms");
    }
}

达观锁改善

好麻烦呀,咱们自己调用原生的UnSafe类完成达观锁,有什么更好的方式吗?

当然有,其实JDK给咱们封装了许多根据UnSafe达观锁完成的原子类,比如AtomicIntegerAtomicReference等等。咱们用AtomicInteger改写下上面的完成。

乐观锁思想在JAVA中的实现——CAS

  • 运用JDK中的原子类AtomicInteger作为余额的类型
  • 取钱逻辑直接调用addAndGet办法

运转成果:

乐观锁思想在JAVA中的实现——CAS

原理:

乐观锁思想在JAVA中的实现——CAS

查看源码终究也是调用的Unsafe办法。

CAS机制

前面的一个取钱的比如,咱们是不是对达观锁的思维以及在JAVA中的完成更深入的知道。

在JAVA中对这种完成起了一个姓名,叫做CAS, 全称Compare And Swap,是不是很形象,先比较,然后再替换。

那CAS的实质是什么?

CAS先比较然后再替换,感觉是有2步,比较和替换,不像是原子性操作,假如不是原子性操作问题就可大了。实际上,CAS实质对应的是一条指令,是原子操作

CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够确保【比较-交流】的原子性。

着重一点,CAS 有必要凭借 volatile 才能读取到共享变量的最新值来完成【比较并交流】的作用,由于volatile会确保变量的可见性。

总结

结合 CAS 和 volatile 能够完成无锁并发,适用于线程数少、多核 CPU 的场景或许读多写少的场景。

  • CAS 是根据达观锁的思维:最达观的估量,不怕别的线程来修正共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是根据失望锁的思维:最失望的估量,得防着其它线程来修正共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有时机。
  • CAS 表现的是无锁并发、无堵塞并发,请细心体会这两句话的意思
    • 由于没有运用 synchronized,所以线程不会陷入堵塞,这是功率提升的要素之一
    • 但假如竞赛激烈,能够想到重试必然频频产生,反而功率会受影响

假如本文对你有协助的话,请留下一个赞吧

本文正在参加「金石方案 . 分割6万现金大奖」