• Java中锁的解决方案


    1、:乐观锁 与 悲观锁

    乐观锁与悲观锁应该是每个开发人员最先接触的两种锁。应用场景主要是在更新数据的时候,更新数据这个场景也是使用锁的非常主要的场景之一。更新数据的主要流程如下:

    1. 检索出要更新的数据,供操作人员查看;
    2. 操作人员更改需要修改的值
    3. 点击保存,更新数据

    这个流程看起来很简单,但是我们用多线程的思维去考虑,这也应该算是一种互联网思维吧,就会发现其中隐藏的问题。我们具体看一下

    1. A检索出数据
    2. B检索出数据
    3. B修改了数据
    4. A修改数据,系统会修改成功吗?

    1:乐观锁

    当然了,A修改成功与否,要看程序怎么写。咱们抛开程序,从常理考虑,A保存数据的时候,系统要给提示,说您要修改的数据已被其他人修改过,请重新查询确认。那么我们程序中怎么实现呢?

    1. 在检索数据,将数据的版本号或者最后更新时间一并查询出来
    2. 操作员更改数据以后,点击保存,在数据库执行update 操作
    3. 在执行update 操作时,用步骤1 查询出的版本号或者最后更新时间与数据库中的记录进行比较
    4. 如果版本号或最后更新时间一致,则可以更新
    5. 如果不一致,就要给出上面的提示
    update xx set number = 10 , revision = #{revision} + 1  where id = #{id} and revision = #{revision}

    上述的流程就是乐观锁的实现方式。在JAVA中乐观锁并没有确定的方法 ,或者关键字,它只是一个处理的流程、策略。咱们看懂上面的例子之后,再来看看JAVA中乐观锁。

    乐观锁呢,它是假设一个线程在取数据的时候不会被其他线程更改数据,就像上面的例子那样,但是在更新数据的时候会校验数据有没有被修改过。它是一种比较交换的机子,简称CAS(Compare And Swap)机制。不是很熟悉的很容易和 CAP(Consistency Availability Partition tolerance)定理 搞混淆。CAS机制 一旦检测到有冲突产生,也就是上面说到的版本号或者最后更新时间不一致,它就会进行重试,直到没有冲突为止。

    乐观锁的机制如图所示:

     咱们看一下JAVA中最常用的 i++,我们思考一个问题,i++ 它的执行顺序是什么样子的?它是线程安全的吗?当多个线程并发执行i++ 的时候,会不会有问题?接下来咱们通过程序看一下

    package com.bfxy.esjob;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * @Author: qiuj
     * @Description:    乐观锁
     * @Date: 2020-06-27 13:43
     */
    public class OptimisticLocking {
    
        private int i = 0;
    
        public static void main(String[] args) throws InterruptedException {
            new OptimisticLocking().notOptimisticLocking();
        }
    
    
        public void notOptimisticLocking () throws InterruptedException {
            OptimisticLocking optimisticLocking = new OptimisticLocking();
            ExecutorService executorService = Executors.newFixedThreadPool(50);
            //  目的等5000个任务执行完在执行 主线程的输出语句
            CountDownLatch countDownLatch = new CountDownLatch(5000);
            for (int i = 0; i < 5000; i++) {
                executorService.execute(() -> {
                    optimisticLocking.i++;
                    //  5000计数器减1
                    countDownLatch.countDown();
                });
            }
            //  执行完任务将线程池关闭
            executorService.shutdown();
            //  5000个任务执行完,放开主线程执行输出语句
            countDownLatch.await();
            System.out.println("执行完成后,i=" + optimisticLocking.i);
            /*
            i++ 不是原子性的  线程不安全的
            1: 取出当前的值  例如 2000
            2: 修改为2001 ,但是在这时候并不只是这个线程在执行相同步骤。存在并发性。所以有可能值被重复覆盖了
             */
        }
    }
    

    上面的程序中,我们模拟了50个线程同时执行 i++ ,总共执行5000次,按照常规的理解, 得到的结果应该是5000,我们运行一下程序,看看执行的结果如何?

    执行完成之后,i=4993

    执行完成之后,i=4996

    执行完成之后,i=4988

     这是我们运行3次以后得到的结果,可以看到每次执行的结果都不一样,而且不是5000,这是为什么呢?这就说明 i++ 并不是一个原子性的操作,在多线程的情况下并不安全。我们把 i++ 的详细执行步骤拆解一下:

    1. 从内存中取出 i 的当前值
    2. 将 i 的值加1
    3. 将计算好的值放入到内存当中

    这个流程和我们上面讲解的数据库的操作流程是一样的。在多线程的场景下,我们可以想象一下,线程A和线程B同时从内存取出 i  的值,假如 i 的值是 1000 ,然后线程A和线程B再同时执行 +1 操作,然后把值再放入内存中,这时,内存中的值是1001,而我们期望的是1002,正是这个原因导致了上面的错误。那么我们如何解决呢?在JAVA1.5以后,JDK官方提供了大量的原子类,这些类的内部都是基于 CAS机制的,也就是使用了 乐观锁。我们将上面的程序稍微改造一下,如下:

    package com.bfxy.esjob;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * @Author: qiuj
     * @Description:    乐观锁
     * @Date: 2020-06-27 13:43
     */
    public class OptimisticLocking {
    
        private int i = 0;
    
        private AtomicInteger atomicInteger = new AtomicInteger(0);
    
        public static void main(String[] args) throws InterruptedException {
    //        new OptimisticLocking().notOptimisticLocking();
            new OptimisticLocking().optimisticLocking();
        }
    
    
        public void notOptimisticLocking () throws InterruptedException {
            OptimisticLocking optimisticLocking = new OptimisticLocking();
            ExecutorService executorService = Executors.newFixedThreadPool(50);
            //  目的等5000个任务执行完在执行 主线程的输出语句
            CountDownLatch countDownLatch = new CountDownLatch(5000);
            for (int i = 0; i < 5000; i++) {
                executorService.execute(() -> {
                    optimisticLocking.i++;
                    //  5000计数器减1
                    countDownLatch.countDown();
                });
            }
            //  执行完任务将线程池关闭
            executorService.shutdown();
            //  5000个任务执行完,放开主线程执行输出语句
            countDownLatch.await();
            System.out.println("执行完成后,i=" + optimisticLocking.i);
            /*
            i++ 不是原子性的  线程不安全的
            1: 取出当前的值  例如 2000
            2: 修改为2001 ,但是在这时候并不只是这个线程在执行相同步骤。存在并发性。所以有可能值被重复覆盖了
             */
        }
    
        public void optimisticLocking () throws InterruptedException {
            OptimisticLocking optimisticLocking = new OptimisticLocking();
            ExecutorService executorService = Executors.newFixedThreadPool(50);
            CountDownLatch countDownLatch = new CountDownLatch(5000);
            for (int i = 0; i < 5000; i++) {
                executorService.execute(() -> {
                    optimisticLocking.atomicInteger.incrementAndGet();
                    countDownLatch.countDown();
                });
            }
            executorService.shutdown();
            countDownLatch.await();
            System.out.println("执行完成后,i=" + optimisticLocking.atomicInteger);
        }
    }
    

    我们将变量 i 的类型改为 AtomicInteger ,AtomicInteger 是一个原子类。我们在之前调用 i++ 的地方改为了 i.incrementAndGet(),incrementAndGet() 方法采用了 CAS 机制,也就是说使用了 乐观锁。我们在运行一下程序,看看结果如何

    执行完成后,i=5000

    执行完成后,i=5000

    执行完成后,i=5000

    我们同样执行了 3 次, 3次的结果都是 5000 ,符合了我们预期。这就是乐观锁。我们对乐观锁稍加总结,乐观锁在读取数据的时候不会做任何限制,而是在更新数据的时候,进行数据的比较,保证数据的版本一致时再更新数据。根据它的这个特点,可以看出乐观锁适用于读操作多,而写操作少的场景。

    2:悲观锁

    悲观锁与乐观锁恰恰相反,悲观锁从读取数据的时候就显示的加锁,直到数据更新完成,释放锁为止,在这期间只能有一个线程去操作,其他的线程只能等待。在JAVA中,悲观锁可以使用 synchronized 关键字或者 ReentrantLock 类来实现。还是上面的例子,我们分别使用这两种方式来实现一下。首先是使用  synchronized 关键字来实现:

    package com.bfxy.esjob;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * @Author: qiuj
     * @Description:    悲观锁
     * @Date: 2020-06-27 15:08
     */
    public class PessimisticLocking {
        private Integer i = 0;
    
        public static void main(String[] args) throws InterruptedException {
            new PessimisticLocking().synchronizedPessimisticLocking();
        }
    
        public void synchronizedPessimisticLocking () throws InterruptedException {
            PessimisticLocking pessimisticLocking = new PessimisticLocking();
            ExecutorService executorService = Executors.newFixedThreadPool(50);
            CountDownLatch countDownLatch = new CountDownLatch(5000);
            for (int i = 0; i < 5000; i++) {
                executorService.execute(() -> {
                    synchronized (pessimisticLocking) {
                        pessimisticLocking.i++;
                    }
                    countDownLatch.countDown();
                });
            }
            executorService.shutdown();
            countDownLatch.await();
            System.out.println("执行完成后,i=" + pessimisticLocking.i);
        }
    
    }

     我们唯一的改动就是增加了 synchronized 块,它锁住的对象是 test ,在所有线程中,谁获得了 test 对象的锁,谁才能执行 i++ 操作。我们使用了 synchronized 悲观锁的方式,使得 i++ 线程安全。我们运行一下,看看结果如何

    执行完成后,i=5000

    执行完成后,i=5000

    执行完成后,i=5000

    我们运行3次,结果都是 5000,符合预期,接下来,我们再使用 ReentrantLock 类来实现悲观锁。代码如下:

    package com.bfxy.esjob;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @Author: qiuj
     * @Description:    悲观锁
     * @Date: 2020-06-27 15:08
     */
    public class PessimisticLocking {
        private Integer i = 0;
        private Lock lock = new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
    //        new PessimisticLocking().synchronizedPessimisticLocking();
            new PessimisticLocking().reentrantLockPessimisticLocking();
        }
    
        public void synchronizedPessimisticLocking () throws InterruptedException {
            PessimisticLocking pessimisticLocking = new PessimisticLocking();
            ExecutorService executorService = Executors.newFixedThreadPool(50);
            CountDownLatch countDownLatch = new CountDownLatch(5000);
            for (int i = 0; i < 5000; i++) {
                executorService.execute(() -> {
                    synchronized (pessimisticLocking) {
                        pessimisticLocking.i++;
                    }
                    countDownLatch.countDown();
                });
            }
            executorService.shutdown();
            countDownLatch.await();
            System.out.println("执行完成后,i=" + pessimisticLocking.i);
        }
    
        public void reentrantLockPessimisticLocking () throws InterruptedException {
            PessimisticLocking pessimisticLocking = new PessimisticLocking();
            ExecutorService executorService = Executors.newFixedThreadPool(50);
            CountDownLatch countDownLatch = new CountDownLatch(5000);
            for (int i = 0; i < 5000; i++) {
                executorService.execute(() -> {
                    pessimisticLocking.lock.lock();
                    pessimisticLocking.i++;
                    pessimisticLocking.lock.unlock();
                    countDownLatch.countDown();
                });
            }
            executorService.shutdown();
            countDownLatch.await();
            System.out.println("执行完成后,i=" + pessimisticLocking.i);
        }
    
    }

    我们再类中显示的增加了 Lock lock = new ReentrantLock(); 而且在 i++ 之前增加了 lock.lock() 加锁操作,在 i++ 之后增加了 lock.unlock() 释放锁的操作。我们同样运行3次,看看结果。

    执行完成后,i=5000

    执行完成后,i=5000

    执行完成后,i=5000 

    三次运行结果都是 5000,完全符合预期。我们再来总结一下 悲观锁,悲观锁从读取数据的时候就加了锁,而且在更新数据的时候, 保证只有一个线程在执行更新操作,并没有像乐观锁那样进行数据版本的比较。所以悲观锁适用于读相对少,写相对多的操作。

    2、:公平锁 与 非公平锁

    前面我们介绍了乐观锁与悲观锁,这一小节我们将从另外一个维度去讲解锁--公平锁与非公平锁。从名字不难看出,公平锁在多线程情况下,对待每一个线程都是公平的;而非公平锁恰好与之相反。从字面上理解还是有些晦涩难懂,我们还是举例说明,场景还是去超市买东西,在储物柜存储东西的例子。储物柜只有一个,同时来了3个人使用储物柜,这时 A 先抢到了柜子,A去使用,B和C自觉进行排队。A 使用完之后,后面排队中的第一个人将继续使用柜子,这就是公平锁。在公平锁当中,所有的线程都自觉排队,一个线程执行完之后,排在后面的线程继续使用。

    非公平锁则不然,A在使用柜子的时候,B 和 C 并不会排队,A 使用完之后,将柜子的钥匙往后面一抛,B 和 C 谁抢到就谁用,甚至可能突然冒出来个 D ,这个 D 抢到了钥匙,那么D 将使用柜子 ,这个就是非公平锁。

    公平锁与非公平锁都在 ReentrantLock 类里给出了实现,我们看一下 ReentrantLock 的源码

        /**
         * Creates an instance of {@code ReentrantLock}.
         * This is equivalent to using {@code ReentrantLock(false)}.
         */
        public ReentrantLock() {
            sync = new NonfairSync();
        }
    
        /**
         * Creates an instance of {@code ReentrantLock} with the
         * given fairness policy.
         *
         * @param fair {@code true} if this lock should use a fair ordering policy
         */
        public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }

     ReentrantLock 有两个构造方法,默认的构造方法中,sync = new Nonfairsync(); 我们可以从字面意思看出它是一个非公平锁。再看看第二个构造方法,它需要传入一个参数,参数是一个布尔型,true 是公平锁,false 是非公平锁。从上面的源码我们可以看出 sync 有两个实现类,分别是 FairSync 和 NonfairSync ,我们再看看获取锁的核心方法,首先是 公平锁  FairSync 的源码

        /**
         * Sync object for fair locks
         */
        static final class FairSync extends Sync {
            private static final long serialVersionUID = -3000897897090466540L;
    
            final void lock() {
                acquire(1);
            }
    
            /**
             * Fair version of tryAcquire.  Don't grant access unless
             * recursive call or no waiters or is first.
             */
            protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
        }

    然后是非公平锁 NonfairSync 的源码

            /**
             * Performs non-fair tryLock.  tryAcquire is implemented in
             * subclasses, but both need nonfair try for trylock method.
             */
            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }

    通过对比两个方法,我们可以看出唯一的不同之处在于 !hasQueuedPredecessors() 这个方法.很明显这个方法是一个队列,由此可以推断,公平锁是将所有的线程放在一个队列中,一个线程执行完成后,从队列中取出下一个线程,而非公平锁则没有这个队列。这些都是公平锁与非公平锁底层的实现原理,我们在使用的时候不用追到这么深层次的代码,只需要了解公平锁与非公平锁的含义,并且在调用构造方法时,传入 true 或 false 即可。

    1:公平锁

    公平锁如图所示:

    多个线程同时执行方法,线程A 抢到了锁,A可以执行方法。其他线程则在队列里进行排队,A 执行完方法后, 会从队列里面取出下一个 线程B ,再去执行方法。以此类推,对于每一个线程来说都是公平的,不会存在说后面加入的线程先执行的情况。

    2:非公平锁

     非公平锁如下图所示:

     多个线程同时执行方法,线程 A 抢到了锁,A 可以执行方法。但是其他线程并不会排队,A 执行完方法,释放锁后,其他的线程谁抢到了锁,那谁就去执行方法。会存在说后面加入的线程,比提前加入的线程,反而先抢到锁的情况。

    3、:总结

    JAVA 中锁的种类非常多,找了非常典型的几个锁的类型介绍了下,乐观锁与悲观锁是最基础的,也是大家必须要掌握的。大家在工作中不可避免的都要使用到乐观锁和悲观锁。从公平锁与非公平锁这个角度上看,大家平时使用的都是非公平锁,这也是默认的锁类型。如果要使用公平锁,大家可以在秒杀的场景下使用,在秒杀的场景下,是遵循先到先得的原则,是需要排队的,所以这种场景下是最适合使用公平锁的。

  • 相关阅读:
    TweenLite简单运用
    nodejs 重定向 (redirect + writeHead(Location))
    Nodejs Web模块( readFile 根据请求跳转到响应html )
    Express框架(http服务器 + 路由)
    AI 学习路线
    implicitly_wait()隐式等待
    Python 爬虫基础Selenium
    Selenium2+python自动化15-select下拉框
    python selenium while 循环
    jupyter notebook修改默认路径和浏览器
  • 原文地址:https://www.cnblogs.com/blogspring/p/14191741.html
Copyright © 2020-2023  润新知