当前位置: 首页 > 科技观察

面试官:工作中用过锁么?说说乐观锁和悲观锁的优劣势和使用场景

时间:2023-03-19 14:25:06 科技观察

面试官:你在工作中用过锁吗?说说乐观锁和悲观锁的优缺点和使用场景。乐观锁和悲观锁能解决什么问题?在并发场景下,有序更新一条记录。什么是乐观锁,什么是悲观锁?乐观锁:乐观锁在操作数据时非常乐观,认为其他人不会同时修改数据。因此,乐观锁不会加锁,只是在更新时判断别人是否修改了数据:如果别人修改了数据,则放弃操作,否则执行操作。悲观锁:悲观锁在操作数据时比较悲观,认为其他人会同时修改数据。所以在操作数据的时候直接加锁,直到操作完成才会释放锁;在锁定期间,其他人不能修改数据。这两把锁应该怎么实现呢?乐观锁的实现方式主要有两种:CAS机制和版本号机制。CAS:(CompareAndSwap)CAS操作包括3个操作数:待读写的内存位置(V)待比较的期望值(A)待写入的新值(B)操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则什么都不做。很多CAS操作都是自旋的:如果操作不成功,会重试,直到操作成功。这就引出了一个新的问题,既然CAS包含了Compare和Swap这两个操作,那么它是如何保证原子性的呢?答案是:CAS是CPU支持的原子操作,其原子性是在硬件层面保证的。注意:Java中的自增操作(i++)在并发场景下无法得到100%准确的结果,因为它不是原子操作。在并发场景下,自增操作应该使用AtomicInteger,其内部顺序也是通过CAS乐观锁自增。版本号机制(Version)版本号机制的基本思想是在数据中加入一个字段version,表示数据的版本号。每当修改数据时,版本号加1。当一个线程查询数据时,一起检查数据的版本号;线程更新数据时,判断当前版本号与之前读取的版本号是否一致,一致才进行操作。需要注意的是,这里使用版本号作为判断数据变化的标志。其实其他可以标记数据版本的字段,比如时间戳,可以根据实际情况选择。悲观锁的两种实现:synchronized和select...forupdate代码实现悲观锁:synchronized。synchronized通过锁定代码块来保证线程安全:同时只有一个线程可以执行代码块中的代码。synchronized是一个重量级的操作,不仅因为加锁需要额外的资源,还因为线程状态的切换会涉及到操作系统核心态和用户态的转换;自旋锁、轻量级锁、锁粗化等),synchronized的性能已经越来越好。SQL实现悲观锁select...forupdate该查询语句会给行记录加排他锁,直到事务提交或回滚后排他锁才会释放;在此期间,如果有其他事务只能锁定该行记录执行查询操作。如果更新了该行的记录信息或者执行了selectforupdate,就会被阻塞。ps:使用selectforupdate的时候一定要跟上whereid=?的条件。id字段必须是主键或唯一索引。否则整张表都会被锁住,后果很严重!分析两种锁的优缺点?乐观锁的优点:轻量级锁,避免了线程切换的开销。缺点:会有ABA问题。假设有两个线程——线程1和线程2。这两个线程依次执行以下操作:线程1读取内存中的数据为A;线程2修改数据给B;线程2将数据修改为A;线程1对第4步的数据进行CAS操作,因为内存中的数据还是A,所以CAS操作成功,但实际上数据已经被线程2修改了,这就是ABA问题。自旋操作只能锁定在单个变量上,导致额外的开销。悲观锁优点:因为锁是一个代码块,可以锁住多个变量。缺点:重量级锁,加锁和释放锁都会有开销,操作系统层面的上下文切换和线程调度也会造成很大的开销。一个线程持有锁会导致所有其他需要锁的线程挂起。不同场景如何选择乐观锁和悲观锁?高并发、高竞争下,为了避免重试开销,直接选择悲观锁。比如订火车票的时候,屏幕上显示有车票,但是真正出票的时候,需要重新确认这个数据没有被其他客户端修改过。因此,在这个确认过程中,可以使用forupdate。并发情况不激烈,偶尔解决并发问题,选择性能消耗低的乐观锁。