一,悲观锁
总是认为线程不安全,不管什么情况都进行加锁,要是获取锁失败,就阻塞。实现:数据库加锁、Java中,synchronized加锁等。多用于写多读少,保证数据安全的场景。
二,乐观锁
总认为不会产生并发问题,因此不会加锁。但在更新时会判断其他线程在这之前有没有对数据进行修改。
实现1:使用版本号
一般是在数据表中加上version字段,表示数据被修改的次数,当数据被修改时,version加一。线程读取数据时也同时会读取version的值,当线程要更新数据时,若刚才读取到的version值与当前数据库中的version值相等时才更新。
SQL:
update tb_goods set amount = amount - #{nums}, version = version + 1 where goods_id = #{goodsId} and version = #{version};
实现2:通过MySQL状态实现
例如秒杀中,判断数据库中商品库存-购买数是否大于或等于0。
SQL:
update tb_goods set amount = amount - #{nums} where goods_id = #{goodsId} and amount - #{nums} >= 0;
这两种乐观锁都是通过数据库实现,简单高效、稳定可靠。但缺点在于并发能力低,并发量阈值在300~700间。
实现3:缓存实现
compare and swap比较与交换。无锁算法,非阻塞同步。CAS算法涉及三个操作数(内存地址V、旧的预期值A、要更新的目标值B),当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。
一般情况下是一个自旋操作,即不断的重试。多用于读多写少,提高系统吞吐量的场景。
Redis实现乐观锁:
//开启事务支持
redisTemplate.setEnableTransactionSupport(true);
//监听key
redisTemplate.watch("key");
//开始事务
redisTemplate.multi();
//执行事务,如果其他线程对key中的value进行修改,则该事务将不会执行
List<Object> list= redisTemplate.exec();
if(list != null ){
//操作成功
}else{
//操作失败
}
三,CAS
AtomicXXX(AtomicInteger/AtomicBoolean等) 就是使用CAS原理。
CAS缺点:
- 如果CAS失败,会一直进行尝试,给CPU带来很大开销;
- 对多个共享变量操作时,循环CAS就无法保证操作的原子性;
- ABA问题:(ABA问题是什么?怎么解决?)
四,ABA问题
ABA问题是什么? 如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。
解决:
Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。
因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
评论区