悲观锁
总是假设最坏的情况,悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能变动,一个事务拿到悲观锁后(可以理解为一个用户),其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)。
传统的关系型数据库里就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。Java中 synchronized
和 ReentrantLock
等独占锁就是悲观锁思想的体现。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
悲观锁主要是共享锁或排他锁。
- 共享锁又称为读锁,简称 S 锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
- 排他锁又称为写锁,简称 X 锁。排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
乐观锁常见的两种实现方式
乐观锁一般会使用版本号机制或CAS算法实现。
版本号机制
一般是在数据表中加一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
CAS 算法
CAS 指令是硬件支持的操作: Compare And Swap(比较与交换),是一种著名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数
- 需要读写的内存地址 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
Java 中的 AtomicInteger 使用 CAS 来实现。
CAS 比较与交换的伪代码可以表示为:
do{
// 备份旧数据;
// 基于旧数据构造新数据;
} while(!CAS( 内存地址,备份的旧数据,新数据 ))
乐观锁的缺点
ABA 问题
如果一个变量 V 初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 ABA 问题。
Java 并发包为了解决这个问题,提供了一个带有标记的原子引用类 AtomicStampedReference
,它可以通过控制变量值的版本来保证 CAS 的正确性。其中的 compareAndSet
方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大
自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。
只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以可以使用锁或者利用 AtomicReference
类把多个共享变量合并成一个共享变量来操作。
优缺点
悲观锁
- 优点:悲观锁利用数据库中的锁机制来实现数据变化的顺序执行,这是最有效的办法
- 缺点:效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的可能。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。一个事务用悲观锁对数据加锁之后,其他事务将不能对加锁的数据进行除了查询以外的所有操作,如果该事务执行时间很长,那么其他事务将一直等待,那势必影响系统的吞吐量。
乐观锁
- 优点:乐观锁不在数据库上加锁,任何事务都可以对数据进行操作,在更新时才进行校验,这样就避免了悲观锁造成的吞吐量下降的劣势。
- 缺点:乐观锁因为时通过人为实现的,它仅仅适用于自己的业务中,如果有外来事务插入,那么就可能发生错误。
应用场景
悲观锁:悲观锁会影响系统吞吐的性能,所以适合应用在写居多的场景下。悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
乐观锁:乐观锁是为了避免悲观锁的弊端出现的,所以适合应用在读居多的场景下。乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。