前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试系列之-悲观锁和乐观锁(JAVA基础)

面试系列之-悲观锁和乐观锁(JAVA基础)

作者头像
用户4283147
发布2023-09-11 15:53:59
1580
发布2023-09-11 15:53:59
举报
文章被收录于专栏:对线JAVA面试对线JAVA面试

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。在Java中,synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁; 手动加悲观锁 读锁:LOCK tables test_db read,释放锁:UNLOCK TABLES; 写锁:LOCK tables test_db WRITE,释放锁:UNLOCK TABLES; 读锁与写锁 如果要更新数据,那么加锁的时候就直接加写锁,一个线程持有写锁的时候别的线程无论读还是写都需要等待; 如果是读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1); 虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁; ●优点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高; ●缺点:因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下,在此期间有没有别人去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS;

形象化记忆:乐观锁认为对数据的操作不会产生冲突,所以不会加锁,而是在提交更新的时候才会对数据的冲突与否进行检测。如果发现冲突,要么再重试一次,要么切换为悲观的策略。乐观并发控制要解决的是数据库并发场景下的写-写冲突,指用无锁的方式去解决。

乐观锁是一种并发类型的锁,其本身并不对数据进行加锁,而是通循环重试CAS进而实现锁的功能,其不对数据进行加锁就意味着允许多个线程同时读取(因为根本没有加锁操作)数据,但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试,这种方式大大的提高了数据操作的性能,因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程;乐观锁常见的两种实现方式

乐观锁一般会使用版本号机制或CAS算法实现;实现机制

t1和t2线程同时去修改内存中的同一变量56,他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56; 假设t1在与t2线程竞争中,线程t1能去更新变量的值,而其他线程都失败(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试); t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值);

通俗的解释是:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升;CAS算法的缺点

ABA 问题及其 解决方案thread1意图对val=1进行操作变成2,cas(val,1,2);thread1先读取val=1;可是thread1被抢占,让thread2运行;thread2修改val=3,又修改回1。thread1继续执行,发现期望值与“原值”(其实被修改过了)相同,完成CAS操作;添加额外的标记用来指示是否被修改:从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值;循环时间长,开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率;只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作;悲观锁机制存在以下问题

●在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题; ●一个线程持有锁后,会导致其他所有抢占此锁的线程挂起; ●如果一个优先级高的线程等待一个优先级低的线程释放锁;

就会导致线程的优先级倒置,从而引发性能风险。解决以上悲观锁的这些问题的有效方式是使用乐观锁去替代悲观锁。与之类似数据库操作中的带版本号数据更新、JUC包的原子类,都使用了乐观锁的方式提升性能;两者的比较

1乐观锁并未真正加锁(不是真正的锁),效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败; 2悲观锁依赖数据库锁,效率低。更新失败的概率比较低; 3悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适;

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-08-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 对线JAVA面试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档