Please tell me the difference between pessimistic locking and optimistic locking

Pessimistic locks and optimistic locks are not a specific "lock" but a basic concept of concurrent programming, based on the perspective of concurrent synchronization. Optimistic locking and pessimistic locking first appeared in database design, and were gradually introduced by Java's concurrent package.

pessimistic lock

Pessimistic locks believe that concurrent operations on the same data must be modified, and take the form of locking. Pessimistically believe that concurrent operations without locks will inevitably cause problems. Many such lock mechanisms are used in traditional relational databases, such as row locks, table locks, etc., read locks, write locks, etc., which are all locked before performing operations. Exclusive locks such as Synchronized and ReentrantLock in Java are implemented by pessimistic locking.

optimistic locking

Optimistic locking is just the opposite of pessimistic locking. When it acquires data, it does not worry about the data being modified, and it does not lock each time it acquires data, but when updating data, it determines whether the existing data is the same as the original data. It is consistent to judge whether the data is operated by other threads. If it is not modified by other threads, it will update the data. If it is modified by other threads, it will not update the data. Lock is a typical implementation case of optimistic locking.

Typical Case

(1) Pessimistic lock: synchronized keyword and Lock interface related classes

The implementation of pessimistic locks in Java includes the synchronized keyword and Lock related classes. Let's take the Lock interface as an example. For example, the implementation class ReentrantLock of Lock, the lock() method in the class is to execute the lock, and the unlock() method is to execute Unlock. Before processing resources, you must first lock and obtain the lock, and then unlock the lock after processing, which is a very typical pessimistic locking idea.

(2) Optimistic lock: atomic class

A typical case of optimistic locking is atomic classes. For example, AtomicInteger uses the idea of ​​optimistic locking when updating data. Multiple threads can operate on the same atomic variable at the same time.

(3) Great joy and great sorrow: database

There are both pessimistic locking and optimistic locking in the database. For example, if we choose the select for update statement in MySQL, it is a pessimistic lock, and a third party is not allowed to modify the data before committing, which of course will cause a certain performance loss, which is not desirable in the case of high concurrency. Instead, we can implement optimistic locking in the database using a version field. There is no need to lock when acquiring and modifying data, but when we are ready to update the data after acquiring and modifying the data, we will check whether the version number is the same as the version number when acquiring the data. If it is consistent, we will update it directly. It means that other threads have modified the data during the calculation, so I can choose to re-fetch the data, re-calculate, and then try to update the data again.

An example of a pessimistic lock for a database:

select * from account where name="Erica" for update

This sql statement locks all records in the account table that meet the retrieval conditions ( name="Erica" ​​). Before the transaction is committed (the locks in the transaction process will be released when the transaction is committed), the outside world cannot modify these records.

An example of optimistic locking for a database:

Suppose the version is 1 when the data is fetched

UPDATE student

SET

name='Little Li',

version= 2WHERE   id= 100AND version= 1

Introduction to CAS

Most of the optimistic locking in Java is implemented through the CAS (CompareAndSwap, compare and exchange) operation. CAS is a multi-threaded synchronized atomic instruction. The CAS operation contains three important information, namely memory location, expected original value and new value. If the value of the memory location is equal to the expected original value, then the value of the location can be updated to the new value, otherwise no modification is made.

Supplementary note: Although ReentrantLock is also implemented through CAS, it is a pessimistic lock.

Disadvantage: ABA problem.

CAS可能会造成ABA的问题,ABA问题指的是,线程拿到了最初的预期原值A,然而在将要进行CAS的时候,被其他线程抢占了执行权,把此值从A变成了B,然后其他线程又把此值从B变成A,然而此时的 A 值已经并非原来的 A 值了,但最初的线程并不知道这个情况,在它进行 CAS 的时候,就会误认为它从来没有被修改过,只对比了预期原值为 A 就进行了修改,这就造成了 ABA 的问题。

以警匪剧为例,假如某人把装了100W现金的箱子放在了家里,几分钟之后要拿它去赎人,然而在趁他不注意的时候,进来了一个小偷,用空箱子换走了装满钱的箱子,当某人进来之后看到箱子还是一模一样的,他会以为这就是原来的箱子,就拿着它去赎人了,这种情况肯定有问题,因为箱子已经是空的了,这就是 ABA 的问题。

JDK在1.5时提供了AtomicStampedReference类也可以解决ABA的问题,此类维护了一个“版本号”Stamp,每次在比较时不止比较当前值还比较版本号,这样就解决了 ABA 的问题。

综合分析实例:

如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作 员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可见,如果面对几百上千个并发,这样的情况将导致怎样的后果。乐观锁机制在一定程度上解决了这个问题。

乐观锁,大多是基于数据版本( version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。同时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号等于数据库表当前版本号,则予以更新,否则认为是过期数据。

对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

1 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 50 ( 50( 100-$50 )。

2 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 20 ( 20 ( 100-$20 )。

3 操作员 A 完成了修改工作,将 version=1 的数据连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,同时数据库记录 version 更新为 2(set version=version+1 where version=1) 。

4 操作员 B 完成了数据录入操作,也将 version=1 的数据试图向数据库提交( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

悲观锁乐观锁优缺点:

优点:

悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。

缺点:

需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。

使用场景

有一种说法认为,悲观锁由于它的操作比较重量级,不能多个线程并行执行,而且还会有上下文切换等动作,所以悲观锁的性能不如乐观锁好,应该尽量避免用悲观锁,这种说法是不正确的。因为虽然悲观锁确实会让得不到锁的线程阻塞,但是这种开销是固定的。悲观锁的原始开销确实要高于乐观锁,但是特点是一劳永逸,就算一直拿不到锁,也不会对开销造成额外的影响。反观乐观锁虽然一开始的开销比悲观锁小,但是如果一直拿不到锁,或者并发量大,竞争激烈,导致不停重试,那么消耗的资源也会越来越多,甚至开销会超过悲观锁。所以,同样是悲观锁,在不同的场景下,效果可能完全不同。

(1) 悲观锁适合用于并发写入多、临界区代码复杂、竞争激烈等场景,这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。

(2) 乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。在这些场景下,乐观锁不加锁的特点能让性能大幅提高。

Guess you like

Origin juejin.im/post/7087436837911789576