目录
什么是乐观锁和悲观锁
乐观锁的实现方式主要有两种:CAS机制和版本号机制
1)CAS(Compare And Swap)
(2)版本号
乐观锁适用场景
乐观锁和悲观锁优缺点
功能限制
竞争激烈程度
什么是乐观锁和悲观锁
乐观锁:
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量
- 像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。
- 在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等MySQL中的排它锁,都是在做操作之前先上锁。
- 再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁的实现方式主要有两种:CAS机制和版本号机制
1)CAS(Compare And Swap)
CAS操作包括了3个操作数:
- 需要读写的内存位置(V)
- 进行比较的预期值(A)
- 拟写入的新值(B)
CAS操作逻辑如下:
如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。
以Java中的自增操作(i++)为例,看一下悲观锁和CAS分别是如何保证线程安全的。
- CAS :使用乐观锁
AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性
private static AtomicInteger valueCas= new AtomicInteger(0);
...
valueCas.getAndIncrement();
- 使用悲观锁
private static int value = 0;
...
private static synchronized void increaseValue() { value++; }
(2)版本号
版本号机制:乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败
- 使用版本号
update user set name = 'a' ,version = version +1 where id = 1 and version = 1; 条件中version=1 可以确保没有任何事务更新过记录,否则会更新失败。
- 版本号的取值: 可以使用时间戳
同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
- 使用悲观锁
悲观锁通过加锁,使用select …… for update进行查询
比如:SELECT * FROM CONTRACT where id = ** FOR UPDATE,这样就会对该行记录加上X锁。
需要注意的是,SELECT ... FOR UPDATE会对所有扫描过的行加锁,因此在MySql中使用悲观锁要走索引。另外使用SELECT *** FOR UPDATE加上X锁后,仍然可以使用SELECT * from contract where id =...查询出同一行记录。
乐观锁适用场景
比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
实现方式: 相比于悲观锁,在对数据库进行更新时,乐观锁并不会使用数据库提供的锁机制,而是在业务层进行控制。
乐观锁和悲观锁优缺点
乐观锁和悲观锁并没有优劣之分,它们有各自适合的场景;下面从两个方面进行说明。
功能限制
与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。
再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
竞争激烈程度
如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:
当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。