悲观锁和乐观锁浅谈
乐观锁和悲观锁的比较和探究
平时我们在多线程并发编程的情况下经常要使用到锁机制,本文主要讨论了常用的悲观锁和乐观锁机制,同时乐观锁中使用的CompareAndSet(CAS)跟踪了源码并进行一定的分析。
悲观锁(Pessimistic Lock)
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
我们认为系统中的并发更新会非常频繁,并且事务失败了以后重来的开销很大,这样以来,我们就需要采用真正意义上的锁来进行实现。悲观锁的基本思想就是每次一个事务读取某一条记录后,就会把这条记录锁住,这样其它的事务要想更新,必须等以前的事务提交或者回滚解除锁。
实现方式:
平时大多在数据库层面实现加锁操作,比如JDBC方式:在JDBC中使用悲观锁,需要使用select for update语句
e.g. Select * from Account where ...(where condition).. for update.
乐观锁(Optimistic Lock)
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
我们认为系统中的事务并发更新不会很频繁,即使冲突了也没事,大不了重新再来一次。它的基本思想就是每次提交一个事务更新时,我们想看看要修改的东西从上次读取以后有没有被其它事务修改过,如果修改过,那么更新就会失败。
实现方式:
大多是基于数据版本(Version)记录机制实现,何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
假如系统中有一个User的实体类,我们在User中多加一个version字段,那么我们JDBC Sql语句将如下写:
e.g. Select u.version....from User as u where (where condition..)
Update User set version = version+1 ...(another field) where version = ? ...(another contidition)
这样以来我们就可以通过更新结果的行数来进行判断,如果更新结果的行数为0,那么说明实体从加载以来已经被其它事务更改了,所以就抛出自定义的乐观锁定异常(或者也可以采用Spring封装的异常体系),具体实例如下:
......
int rowsUpdated = statement.executeUpdate(sql);
if (rowsUpdated == 0) {
throws new OptimisticLockingFailureException();
}
......
平时我们在Java代码中使用Synchronized进行跨线程的同步的方式就属于悲观锁,它有一个明显的缺点,它不管数据之间存不存在竞争都会加锁,随着并发量的增加,且如果锁的时间比较长,其性能开销将会变得很大。有没有办法解决这个问题?
答案就是基于冲突检测的乐观锁。这种模式下,已经没有所谓的锁概念了,每条线程都直接先去执行操作,计算完成后检测是否与其他线程存在共享数据竞争,如果没有则让此操作成功,如果存在共享数据竞争则可能不断地重新执行操作和检测,直到成功为止,这种叫做CAS自旋。
Java里的CompareAndSet(CAS)
以AtomicInteger的incrementAndGet的实现
1. incrementAndGet的实现
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
}
}
首先可以看到他是通过一个无限循环(spin)直到increment成功为止。
循环的内容是
1.取得当前值
2.计算+1后的值
3.如果当前值还有效(没有被)的话设置那个+1后的值
4.如果设置没成功(当前值已经无效了即被别的线程改过了),再从1开始。
2. compareAndSet的实现
public final boolean compareAndSet(int expect, int pdate) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这里直接调用的是UnSafe这个类的compareAndSwapInt方法,全称是sun.misc.Unsafe。这个类是Oracle(Sun)提供的实现,在别的公司的JDK里就不是这个类了。
3. compareAndSwapInt的实现
/**
* Atomically update Java variable to x if it is currently
* holding expected.
* @return true if successful
*/
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
此方法不是Java实现的,而是通过JNI调用操作系统的原生程序,这涉及到CPU原子操作,现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是CMPXCHG汇编指令。
CAS原子操作在维基百科中的代码描述如下(不要问我怎么FQ...):
int compare_and_swap(int* reg, int oldval, int newval) {
ATOMIC();
int old_reg_val = *reg;
if (old_reg_val == oldval)
*reg = newval;
END_ATOMIC();
return old_reg_val;
}
这段实现也就是检查内存*reg里的值是不是oldval,如果是的话,则对其赋值newval。上面的代码总是返回old_reg_value,调用者如果需要知道是否更新成功还需要做进一步判断,为了方便,它可以变种为直接返回是否更新成功,如下:
bool compare_and_swap (int *accum, int *dest, int newval) {
if ( *accum == *dest ) {
*dest = newval;
return true;
}
return false;
}
悲观锁和乐观锁这两种锁各有优缺点,不可认为一种好于另一种,比如乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,从而加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。