Java中锁的解决方案
1、:乐观锁 与 悲观锁
乐观锁与悲观锁应该是每个开发人员最先接触的两种锁。应用场景主要是在更新数据的时候,更新数据这个场景也是使用锁的非常主要的场景之一。更新数据的主要流程如下:
- 检索出要更新的数据,供操作人员查看;
- 操作人员更改需要修改的值
- 点击保存,更新数据
这个流程看起来很简单,但是我们用多线程的思维去考虑,这也应该算是一种互联网思维吧,就会发现其中隐藏的问题。我们具体看一下
- A检索出数据
- B检索出数据
- B修改了数据
- A修改数据,系统会修改成功吗?
1:乐观锁
当然了,A修改成功与否,要看程序怎么写。咱们抛开程序,从常理考虑,A保存数据的时候,系统要给提示,说您要修改的数据已被其他人修改过,请重新查询确认。那么我们程序中怎么实现呢?
- 在检索数据,将数据的版本号或者最后更新时间一并查询出来
- 操作员更改数据以后,点击保存,在数据库执行update 操作
- 在执行update 操作时,用步骤1 查询出的版本号或者最后更新时间与数据库中的记录进行比较
- 如果版本号或最后更新时间一致,则可以更新
- 如果不一致,就要给出上面的提示
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++ 的详细执行步骤拆解一下:
- 从内存中取出 i 的当前值
- 将 i 的值加1
- 将计算好的值放入到内存当中
这个流程和我们上面讲解的数据库的操作流程是一样的。在多线程的场景下,我们可以想象一下,线程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 中锁的种类非常多,找了非常典型的几个锁的类型介绍了下,乐观锁与悲观锁是最基础的,也是大家必须要掌握的。大家在工作中不可避免的都要使用到乐观锁和悲观锁。从公平锁与非公平锁这个角度上看,大家平时使用的都是非公平锁,这也是默认的锁类型。如果要使用公平锁,大家可以在秒杀的场景下使用,在秒杀的场景下,是遵循先到先得的原则,是需要排队的,所以这种场景下是最适合使用公平锁的。