乐观锁常见的两种实现方式和适用场景
1、版本号机制
一般是在数据表中加上一个版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子:
假设数据库中账户信息表中有一个version字段,当前值为1;而当前账户余额字段为100;
1、操作员A此时将其读出(version=1),并从其账户余额中扣除50(100-50);
2、在操作员A操作的过程中,操作员B也读入此用户信息(version=1),并从其账户余额中扣除20(100-20);
3、操作员A完成了修改操作,将数据版本号+1(version=2),连同账户扣除后余额(子段=50),提交至数据库更新,此时由于 提交数据库版本大于数据库当前记录的版本,数据被更新,数据库记录version更新为2.
4、操作员B完成了操作,也将版本号+1(version=2)试图向数据库提交数据(子段=80),但此时比对数据库记录版本时发现,操作员B提交的数据版本号为2,数据库记录当前版本也为2,不满足“提交版本必须大于记录当前版本才能执行更新”的乐观锁策略,因此,操作员B的提交被驳回。
这样就避免了操作员B用基于version=1的旧数据修改的结果覆盖操作员A的操作结果的可能。
2、CAS算法
即compare and swap(比较和互换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。CAS算法涉及到3个操作数。
1、需要读写的内存值V
2、进行比较的值A
3、拟写入的新值B
当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
CAS和synchronized的使用场景
1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充:Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称他为“重量级锁”。但是,在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁以及其他各种优化之后变得在某些情况下并不是那么重了。
synchronized的底层实现主要依靠lock-Free队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下性能远高于CAS。