乐观锁与悲观锁
1.悲观锁
定义:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。在Java中,synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。
手动加悲观锁:
读锁:LOCK tables test_db read,释放锁:UNLOCK TABLES;
写锁:LOCK tables test_db WRITE,释放锁:UNLOCK TABLES;
读锁与写锁
如果要更新数据,那么加锁的时候就直接加写锁,一个线程持有写锁的时候别的线程无论读还是写都需要等待;
如果是读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)
虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。- 特点:
优点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;
缺点:因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;
2.乐观锁
定义:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下,在此期间有没有别人去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS。
形象化记忆:乐观锁认为对数据的操作不会产生冲突,所以不会加锁,而是在提交更新的时候才会对数据的冲突与否进行检测。如果发现冲突,要么再重试一次,要么切换为悲观的策略。乐观并发控制要解决的是数据库并发场景下的写-写冲突,指用无锁的方式去解决。
- 特点:
优点:乐观锁是一种并发类型的锁,其本身并不对数据进行加锁,而是通循环重试CAS进而实现锁的功能,其不对数据进行加锁就意味着允许多个线程同时读取(因为根本没有加锁操作)数据,但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试,这种方式大大的提高了数据操作的性能,因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。
3.两者的比较
- 1、乐观锁并未真正加锁(不是真正的锁),效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
- 2、悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
- 3、悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。