在数据库操作过程中,为了避免两个或多个用户同时对一条数据操作,通常采用锁的机制来来解决数据冲突问题。
同样,在程序流程中为了避免对多线程共享的资源的修改冲突,也采用锁的机制来避免修改冲突
所谓乐观锁,就是相信大部分场景下,不会产生数据修改冲突,所以在读取数据进行修改的时候,不对数据进行加锁,而是在最终提交修改的时候,通过version或CAS机制,检查对数据的修改是否发生了冲突。
乐观锁的适用于读多写少的场景,能提供系统的处理能力,如果在冲突比较概率高的场景使用乐观锁,反而会降低系统的处理能力。
所谓悲观锁,就是认为对数据修改发生冲突的概率比较大,所以在读取数据进行修改的时候,先用“排他写锁”锁住数据,Block其他人的操作,等修改完成后,再释放锁。
此模式比较适用于数据修改冲突发生概率高的场景,但会一定程度降低系统的处理能力。
数据的行锁、表锁、读锁、写锁都属于悲观锁,典型的就是 select * from xxx where id=n for update
命令。
假设有一条订单(order)数据,ID为1,订单状态(status)是已付款,这个时候,商家打开订单列表,准备进行发货;在商家打开订单后,这时候用户在APP端取消了订单,但是商家不知道;商家执行发货的时候;这时候商家操作发货,如果只根据ID进行更新:
update order set status='已发货' where id=1
则会导致取消的订单被发货,此时,使用CAS机制,在更新数据的时候检查订单状态是否正确:
update order set status='已发货' where id=1 and status='已付款'
并通过检查update语句发返回值,可以确认时数据更新是否成功。
Version机制,是在order表中增加一个数字型的version字段,每次查下数据的时候,都带上version字段。更新数据是,把version字段加1。以上述订单为例,比如:
Java中, java.util.concurrent.atomic
包下的原子变量属于使用CAS计算的乐观锁。
public class AtomicInteger extends Number implements java.io.Serializable { private volatile int value; public final int get() { return value; } public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } }
getAndIncrement 采用了CAS机制,每次从内存中读取数据,然后将此数据和 +1 后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
compareAndSet 利用JNI来完成CPU指令的操作:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
unsafe.compareAndSwapInt(this, valueOffset, expect, update)逻辑类似如下:
if (this == expect) { this = update return true; } else { return false; }
而 synchronized
关键字属于悲观锁。
如线程1读取了一个变量的值为A;这时候线程2修改变量的值B;线程3有把变量值改回为A;此时,线程1再去更新此变量,会认为此变量未被其他人更新过,但其实变量已经被更新了多次。
所以CAS是适用于对象子包含单个共享变量的原子操作,对于对象中包含多个共享变量的情况无法保证原子性。
对于资源竞争比较激烈的情况,CAS自旋的概率较大,会导致CPU开销增大,效率会低于 synchronized
。