1、:乐观锁 与 悲观锁

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

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

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

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

1:乐观锁

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

  1. 在检索数据,将数据的版本号或者最后更新时间一并查询出来
  1. 操作员更改数据以后,点击保存,在数据库执行update 操作
  2. 在执行update 操作时,用步骤1 查询出的版本号或者最后更新时间与数据库中的记录进行比较
  3. 如果版本号或最后更新时间一致,则可以更新
  4. 如果不一致,就要给出上面的提示
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释放单张表的锁 java锁表和解锁_i++

 咱们看一下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:公平锁

公平锁如图所示:

java释放单张表的锁 java锁表和解锁_java_02

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

 

2:非公平锁

 非公平锁如下图所示:

java释放单张表的锁 java锁表和解锁_java释放单张表的锁_03

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

3、:总结

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