谈谈Java中的锁机制(悲观锁和乐观锁)

在这里插入图片描述
图片引用自:GitChat

线程是否需要对资源加锁分为两类

悲观锁

悲观锁是一种消极的思想,它总是认为会有最坏的情况出现,它总是认为数据是会被修改的,所以它会在持有资源的时候把资源和数据锁住。这样其他的线程要请求这个资源的时候就会被阻塞,直到悲观锁把资源释放了。悲观锁有许多的应用场景,最常见的就是我们经常使用的的传统关系型数据库的锁机制。比如行锁,表锁,读锁,写锁这些都是在要资源操作之前加锁。悲观锁的实现往往依靠数据库自身实现的。悲观锁适用于读少写多的场景

Java中Synchronized和ReetrantLock等独占锁都是悲观锁思想的实现。

乐观锁

乐观锁体现的是悲观锁的反面。它是一种积极的思想,它总是认为数据是不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被更新过。乐观锁的实现方案一般有两种(版本号机制和CAS)。乐观锁适用于读多写少的场景,这样可以提高系统的并发量。在Java中 java.util.concurrent.atomic下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁实现的两种方式

乐观锁一般会使用CAS或版本号机制实现

1.版本号机制

版本号机制就是在数据库表中加上一个version字段,表示数据被修改的次数,当执行写操作的时候并且写入成功后version=version+1,当某个线程A要更新数据的时候,在读取数据的同时也会读取version值,在提交更新的时候如果当前提交的version值和读取到的version值相同才执行更新,否则重试直到更新成功。

举一个例子:假设数据库的账户中有一个version字段,当前值为1;而当前用户的余额为100元

1.操作人员A此时将其读出(version=1),并将其账户余额扣除50元(100-50)
2.在操作人员A操作完成的过程中,操作人员B也读入了此用户信息(version=1),并将其账户余额扣除20元(100-20)
3.在操作人员A完成了修改工作,将数据版本号加1(version=2),连同账户扣除后的余额50元一并修改到数据库中,此时提交的版本号为2大于数据库中本身存储的版本号的1的。所以允许修改
4.在操作人员B完成了操作后,当他要提交事务的时候,原来读取到的版本号为1,同样将版本号version+1=2,此时如果B执行提交事务操作,那么会发现当前数据库中的数据的版本号是已经被操作人员A修改的数据2,当前的版本号和数据库中的实际版本号相同,不满足“提交的版本号要大于数据库中实际版本号的要求”,所以提交失败。

这样就避免了操作员B用基于version=1的旧数据修改被A修改的过的数据。(可能有点绕)举一个实际的例子说明一下这是个什么问题。

银行账户Bob现在有余额100元,在某一时刻Bob要消费50元,同时不巧的是这一时刻他老婆刷他的卡20元。
那么此时Bob和他老婆发出的请求同时到达了银行的系统上。如果不加上乐观锁那么会发生三种情况:

  • 第一种情况:Bob先消费了50元,此时账户余额上还有50元,他老婆在用剩下的50元,消费20元剩下30元。在不加锁的情况下这种顺序执行是没有问题的,余额20元(正常的)
  • 第二种情况:Bob消费50元的请求,在事务还没提交之前,他老婆消费20元的请求读取到了当前Bob账户为100元,准备将数据库的余额改为80元,但是这个事务也还没有提交。当Bob那边事务提交之后,那么他的账户余额就为50元,这时如果没有乐观锁,那么他老婆在提交事务,将会把余额改为80元。这笔账明显是算不过的呀,我一共消费了80元,最后应该只剩20元,现在还有80元(不正常)。
  • 第三种情况:和第二种一样,不过区别在于是Bob老婆先提交事务,将账户余额改为80元,然后Bob再提交事务将账户余额改为50元。最后余额是50元(不正常)。

所以如果不加上乐观锁,那么在两个线程同时执行的时候会出现三种不同的情况,如果不只是两个线程呢,是百万个,千万的线程在执行呢,那么结果可能就更加不可预料

2.CAS算法

CAS就是compare and swap(比较交换),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作

  • 需要读写内存值V
  • 进行比较的值A
  • 准备写入的值B

当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个自旋操作,即不断重试

CAS的一些缺点:

  • ABA问题-知乎
  • 高并发的情况下,很容易发生并发冲突,如果CAS一直失败,那么就会一直重试,浪费CPU资源
  • 功能限制CAS是能保证单个变量的操作是原子性的,在Java中要配合使用volatile关键字来保证线程的安全;当涉及到多个变量的时候CAS无能为力;除此之外CAS实现需要硬件层面的支持,在Java的普通用户中无法直接使用,只能借助atomic包下的原子类实现,灵活性受到了限制
悲观锁和乐观锁的优缺点和适用的场景

乐观锁和悲观锁没有优劣之分,它们有各自适用的场景;

  • 功能限制与悲观锁相比,乐观锁的使用场景收到了更多的限制,不管是CAS方式还是版本号机制。CAS只可以保证单个变量操作的原子性,当涉及到多个变量的时候CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理,在说版本号机制如果query的时候是针对表1,但是update的时候是针对表2,也很难通过简单的版本号机制来实现。
  • 竞争激烈程度 如果悲观锁和乐观锁都可以使用,那么选择就应该考虑竞争激烈程度;当竞争不是激烈的时候乐观锁更有优势,因为悲观锁会锁住代码块和数据其他线程根本就不能方法,降低了并发量。当竞争激烈的时候悲观锁更有优势一些,在竞争激烈的时候乐观锁是很容易更新失败,而采用的自旋操作不断重试,是很浪费CPU的
发布了15 篇原创文章 · 获赞 39 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_37024565/article/details/104819092