悲观锁(Pessimistic Lock)
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
我们认为系统中的并发更新会非常频繁,并且事务失败了以后重来的开销很大,这样以来,我们就需要采用真正意义上的锁来进行实现。悲观锁的基本思想就是每次一个事务读取某一条记录后,就会把这条记录锁住,这样其它的事务要想更新,必须等以前的事务提交或者回滚解除锁。
实现方式:
大多在数据库层面实现加锁操作,JDBC方式:在JDBC中使用悲观锁,需要使用select for update语句,e.g.
<code class="language-sql hljs ">Select * from Account where ...(where condition).. for update</code>
乐观锁(Optimistic Lock)
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
我们认为系统中的事务并发更新不会很频繁,即使冲突了也没事,大不了重新再来一次。它的基本思想就是每次提交一个事务更新时,我们想看看要修改的东西从上次读取以后有没有被其它事务修改过,如果修改过,那么更新就会失败。
实现方式:
大多是基于数据版本(Version)记录机制实现,何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
假如系统中有一个Account的实体类,我们在Account中多加一个version字段,那么我们JDBC Sql语句将如下写:
<code class="language-sql hljs "><code class="language-sql hljs ">Select a.version....from Account as a where (where condition..) Update Account set version = version+1.....(another field) where version =?...(another contidition)</code></code>
这样以来我们就可以通过更新结果的行数来进行判断,如果更新结果的行数为0,那么说明实体从加载以来已经被其它事务更改了,所以就抛出自定义的乐观锁定异常。具体实例如下:
<code class="language-sql hljs "><code class="language-sql hljs "><code class="language-java hljs ">int rowsUpdated = statement.executeUpdate(sql); if (rowsUpdated ==0 ) { throws new OptimisticLockingFailureException(); }</code></code></code>
典型的乐观锁:基于冲突检测的乐观锁(CAS自旋)
Synchronized互斥锁属于悲观锁,它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。有没有办法解决这个问题?答案就是基于冲突检测的乐观锁。这种模式下,已经没有所谓的锁概念了,每条线程都直接先去执行操作,计算完成后检测是否与其他线程存在共享数据竞争,如果没有则让此操作成功,如果存在共享数据竞争则可能不断地重新执行操作和检测,直到成功为止,这种叫做CAS自旋。
CAS(Compare And Swap)比较并转换
该算法涉及三个数:内存地址V,旧的预期值A,新的预期值B。当且仅当旧的预期值A和内存值V相同时,将内存值改为B,否则什么也不做。 上述的处理过程是一个原子操作(靠硬件来保证)。
如何来理解上面这一段话呢?我们先了解一下乐观锁和悲观锁各自的做事方式,首先,悲观锁的态度是一件事情我必须要能百分之百掌控才能去做,否则就认为这件事情一定会出问题,而乐观锁的态度就是不管什么事情,我都会先尝试去做,大不了最后不成功就是了。
基于CAS的自旋就是典型的乐观锁,程序执行时,线程1从共享内存中取值V并建一个副本A,对A进行计算后将新的值保存为B,然后对A值和内存中的V值进行比较,如果A等于V,则认为内存中的V值没有被其他线程修改过,可以将新值B赋给内存,否则,认为内存中已被其他的线程修改,则重新执行计算操作和检测,知道旧的期望值A等于内存值V为止。
Java并发包java.util.concurrent.*的核心就是CAS自旋原理。如AtomicInteger、AtomicLong等都是基于CAS实现的。
两种锁的比较
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
乐观锁是假定读取的数据,在写之前不会被更新。适用于数据更新不频繁的场景。
相反,当数据更新频繁的时候,乐观锁的效率很低,因为基本上每次写的时候都要重复读写两次以上。
-
对于数据更新频繁的场合,悲观锁效率更高 ;
-
对于数据更新不频繁的场合,乐观锁效率更高;
一般来说如果并发量很高的话,建议使用悲观锁,否则的话就使用乐观锁。
如果并发量很高时使用乐观锁的话,会导致很多的并发事务回滚、操作失败。