美文网首页
由AtomicInteger谈到数据库乐观锁

由AtomicInteger谈到数据库乐观锁

作者: 谜00016 | 来源:发表于2018-07-25 17:19 被阅读23次

一、从一个经典的面试题谈起

相信有过几年的开发经验的人或多或少对AtomicInteger类都会有些亲切吧。AtomicInteger在java.util.concurrent.atomic包下,被称为原子操作类。高并发的情况下,我们可以在不使用锁的前提下(如synchronized关键字,或者Lock类),可以做到保证数据的正确性。那他内部是如何实现的呢?

在回答这个问题之前,我们先看一个非常经典的面试题,问我们如何做到三个线程循环打印ABC10次?其实这个解决思路有很多,可以使用最常见的锁机制那一套(synchronized,notifyAll,wait),也可以使用Lock 和 Condition,还可以使用Semaphore类,今天我们来说一种不使用锁机制的思路。那就是下面要说的AtomicInteger类。先看下具体的代码实现,他是如何实现三个线程循环打印ABC 10次的。

public class AtomicIntegerExample {
    private AtomicInteger sycValue = new AtomicInteger(0);

    private static final int MAX_SYC_VALUE = 3 * 10;

    public static void main(String[] args) {
        AtomicIntegerExample example = new AtomicIntegerExample();
        ExecutorService service = Executors.newFixedThreadPool(3);

        service.execute(example.new RunnableA());
        service.execute(example.new RunnableB());
        service.execute(example.new RunnableC());

        service.shutdown();
    }

    private class RunnableA implements Runnable {

        public void run() {

            while (sycValue.get() < MAX_SYC_VALUE) {
                if (sycValue.get() % 3 == 0) {
                    System.out.println(String.format("第%d遍",
                            sycValue.get() / 3 + 1));
                    System.out.println("A");
                    sycValue.getAndIncrement();
                }
            }

        }
    }

    private class RunnableB implements Runnable {

        public void run() {

            while (sycValue.get() < MAX_SYC_VALUE) {
                if (sycValue.get() % 3 == 1) {
                    System.out.println("B");
                    sycValue.getAndIncrement();
                }
            }

        }
    }

    private class RunnableC implements Runnable {

        public void run() {

            while (sycValue.get() < MAX_SYC_VALUE) {
                if (sycValue.get() % 3 == 2) {
                    System.out.println("C");
                    System.out.println();
                    sycValue.getAndIncrement();
                }
            }

        }
    }
}

代码并不复杂,我们看一下其中的一句关键代码

sycValue.getAndIncrement();

上面的代码执行结果是可以正常依次循环打印ABC10次的。我们要分析他为什么可以实现上述需求。分析过程如下:
我们开启了三个线程,来分别打印ABC,我们暂且就叫ABC线程吧。当三个线程开始抢夺cpu执行权,我们假设C线程抢到了,在run方法中我们可以看到

...
  while (sycValue.get() < MAX_SYC_VALUE) {
                if (sycValue.get() % 3 == 2) {
                    System.out.println("C");
                    System.out.println();
                    sycValue.getAndIncrement();
                }
            }
...

我们来看下其中关键代码,getAndIncrement是AtomicInteger 类中的一个方法,我们进入源码中看看,这句代码到底执行了啥

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

这里有一个很意思的类Unsafe。要说到这个类就得牵扯出另外一个概念,也是今天要说的核心概念:CAS

二、CAS

CAS全称是Compare And Swap翻译过来就是比较交换。我们可以抽象出一个方法cas(V,E,N),其包含3个参数

V表示要更新的变量

E表示预期值

N表示新值

当执行cas函数时,我们会比较V和E的值是否相等,如果不相等,表示预期值和要更新的值不一样说明该操作已经有其他线程完成了,当前线程无需做任何操作。如果相等,则表示预期值和当前要更新的值一样,则说明此时是可以进行cas操作的。
回到代码这里,

利用Unsafe类的JNI本地方法实现,使用CAS指令,来保证读-改-写是一个原子操作。compareAndSwapInt有4个参数,this - 当前AtomicInteger对象,valueOffset- value属性在内存中的位置(需要强调的不是value值在内存中的位置),expect - 预期值,update - 新值,根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。

...
  public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
...

简单点理解,CAS保证数据操作的原子性。我们知道要使线程数据安全还得同时保证数据的可见性,我们查看AtomicInteger源码,会发现其实内部是使用了volatile 关键字来保证数据的可见性。

...
 static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
...

三、AtomicInteger源码阅读总结

所以对AtomicInteger原子类操作为什么在高并发下能保证数据多而不乱,总结为:内部使用cas指令保证了原子性,使用volatile关键字 保证了数据在线程中的可见性

四、数据库乐观锁

既然谈到了CAS操作方式,那我们就谈谈与之相关的数据库乐观锁。为啥呢,因为数据库乐观锁实现方式之一就是使用CAS算法。

在谈到乐观锁之前,我们先看下面一个案例。
现在有张账户表,有账号id和余额money两个字段。一般用户购买商品,系统要做的流程一般如下:
1、查询当前账号余额;
2、根据余额与商品价格判断用户是否可以进行下单购买
3、余额可以支持用户下单,下单之后对账户表中的余额值进行相应的扣除
假设有两个人A、B同时使用同一个账号id为123456的账号购买商品,已知该账号还剩1000元。当A和B都看中了一款价格为800元的商品,他们同时进行了下单。在第一步中,A和B同时下单,对于系统而言,执行了以下sql

select * from table where id=123456

AB此时都得知账户有1000元,于是进行第二步的时候判断都是可以下单的。往下走,进行第三步,更新账户表中的余额值。这下一个流程走下来,AB使用相同的账号购买了2件800的商品,结果账户余额还剩200.这样的结果老板肯定是会亏的裤衩都不剩的。那怎么解决这个问题呢?我们可以使用数据库锁机制来解决。

数据库锁分为悲观锁和乐观锁。
悲观锁,通俗的理解总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。

乐观锁,总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改。

乐观锁有两种方式可以实现,
一种是我们刚刚上面提到的使用CAS操作,当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。注意:这种方式会产生ABA的问题。
还有一种方式,是在数据表中加上一个数据版本号version字段,默认为0,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。如在该场景中,我们在账户表中添加一个version字段,在第一步和第二步中和前面结果是一样的,当进行第三步的时候,加入A先执行了更新账户表余额操作,那么此时该账户的version为1,当B执行的时候,本质是执行以下sql

//因为在第一步查询的时候,B得到的version值为0
update table set money =200 where id =123456 and version=0

很显然此时B无法更新该账户的数据,因为此时version已经在A更新的时候增加了1,此时version是1。这样我们就可以通过update的记录数得知其是否更新成功。

相关文章

  • 由AtomicInteger谈到数据库乐观锁

    一、从一个经典的面试题谈起 相信有过几年的开发经验的人或多或少对AtomicInteger类都会有些亲切吧。Ato...

  • 乐观锁、悲观锁(共享锁S和排他锁X)总结

    乐观锁 乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作...

  • 锁的概述

    乐观锁与悲观锁 悲观锁 乐观锁和悲观锁的概念出自数据库,但在java并发包中也引入和类似的概念(乐观锁/悲观锁是一...

  • 数据库相关

    一、乐观锁和悲观锁 数据库实现并发控制的手段一般分为乐观锁和悲观锁。 悲观锁: 当我们要对一个数据库中的一条...

  • MySQL数据库InnoDB引擎的读写锁、事务隔离级别实例分析

    数据库锁分类 按性能分 乐观锁(比如使用version字段比对,无需等待)、悲观锁(需要等待其他事务) 乐观锁,如...

  • 乐观锁,悲观锁,防重表的思考

    乐观锁: java当中 cas属于 乐观锁 数据库中一般通过 version 版本号实现 悲观锁: java当中 ...

  • mysql进阶-行级锁、表级锁、乐观锁、悲观锁

    从应用的角度来看数据库锁可分为悲观锁、乐观锁从数据库(InnoDB)的角度看,数据库锁可以分为行级锁和表级锁 1....

  • MySQL05

    数据库高级对象,锁,权限管理 视图 索引 触发器 存储过程 悲观锁与乐观锁 行级锁、表级锁、页锁 数据库权限管理 ...

  • 九、【Java 并发】锁的概述

    乐观锁 & 悲观锁 乐观锁和悲观锁是在数据库中引入的名词,但是在 Java 并发包锁里面也引入了类似的思想。 悲观...

  • 秒杀系统技术方案演变过程

    前言:秒杀系统需要保证商品库存不能出现超卖现象。一、数据库锁机制(悲观锁、乐观锁)实现秒杀(1)悲观锁:数据库本身...

网友评论

      本文标题:由AtomicInteger谈到数据库乐观锁

      本文链接:https://www.haomeiwen.com/subject/cszumftx.html