悲观锁和乐观锁是一种思想
乐观锁:并发冲突几率小,对应模块递归操作简单时使用
悲观锁:并发几率大,对应模块操作复杂时使用
悲观锁
悲观锁认为对于同一个数据的并发操作一定是会发生修改的,采取加锁的形式,悲观地认为,不加锁的并发操作一定会出问题。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中Synchronized和ReentrantLock等独占锁就是悲观锁思想实现的。
悲观锁实现方式
1.synchronized 锁实现
public class SyncTest {
Object lock = new Object();
public void sync(){
synchronized (lock) {
System.out.println("get lock");
}
}
}
2.ReentrantLock 锁实现
private ReentrantLock lock = new ReentrantLock();
public void run() {
lock.lock();
try {
//do bussiness
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
乐观锁
乐观锁正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁实现方式
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算法(AtomicInteger工具类实现)
即compare and swap(比较和互换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。CAS算法涉及到3个操作数。
1、需要读写的内存值V
2、进行比较的值A
3、拟写入的新值B
当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
自旋性能与ABA问题
CAS操作中我们可以看到AtomicInteger类中getAndAddInt方法的自旋操作,如果长时间自旋,那么肯定会对系统造成压力。而且如果value值从A->B->A,那么CAS就会认为这个值没有被操作过,这个称为CAS操作的"ABA"问题。
ABA问题:基于“值”的CAS乐观锁,可能导致ABA问题,应该由“值”比对,优化为“版本号”比对。