乐观锁和悲观锁原理和应用

一、悲观锁

顾名思义,这是一个带有贬义意思的锁,对程序性能方面效率等方面不是很友好,也不是很人性化的一种锁的思想,因为这种锁的利用数据库本身自带的锁,将资源强行独占,在高并发下对性能消耗比较大,所以比较炒蛋,实现方式有数据库本身具备的锁机制:共享锁、排他锁、更新锁。

Java在JDK1.5之前都是靠 synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。这就是一种独占锁,独占锁其实就是一种悲观锁,所以可以说 synchronized 也是悲观锁

1、共享锁(Share Lock)

S锁,也叫读锁,用于所有的只读数据操作。共享锁是非独占的,允许多个并发事务读取其锁定的资源。

  • 多个事务可封锁同一个共享页;
  • 任何事务都不能修改该页;
  • 通常是该页被读取完毕,S锁立即被释放。

例如,执行查询语句“SELECT * FROM my_table LOCK IN SHARE MODE”时,首先锁定第一页,读取之后,释放对第一页的锁定,然后锁定第二页。这样,就允许在读操作过程中,修改未被锁定的第一页。

例如,语句“SELECT * FROM my_table HOLDLOCK”就要求在整个查询过程中,保持对表的锁定,直到查询完成才释放锁定

2、排他锁(Exclusive Lock)

X锁,也叫写锁,表示对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了。

  • 仅允许一个事务封锁此页,一个事务在一行数据加上排他锁后;
  • 其他任何事务必须等到X锁被释放才能对该页进行访问;
  • X锁一直到事务结束才能被释放。

InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁;

使用for update加排他锁:SELECT * FROM my_table FOR UPDATE,Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。

加过排他锁的数据行其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。

3、更新锁

U锁,在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象

4、synchronized(同步锁)

  • 当一个方法使用synchronized修饰后,这个方法变为“同步方法”,此时该方法不允许当一个线程同时到方法内部执行代码。在方法上直接使用synchronized,那么同步监视器对象就是当前方法所属对象,即方法中看到的this。
  • 静态方法若使用synchronized修饰,静态方法的同步监视器对象为这个类的类对象,静态方法里的同步块的同步监视器对象也必须是这个类的类对象
  • 同步块

       synchronized(同步监视器对象){

             需要同步运行的代码判断
       }
同步块可以更准确的控制需要多个线程同步运行的代码片段。有效的缩小同步范围可以在保证并发安全的前提下尽可能的提高并发效率,但同步块在指定同步监视器对象(上锁的对象)时注意,这个对象可以是java任何类型的实例,但是要保证需要同步运行改代码片段的线程看到的是同一个才可以!

  • synchronized作为互斥锁的使用

当使用synchronized控制多段代码,并且他们指定的同步监视器对象是同一个时,这些代码片段互为互斥的,多个线程不能同时在这些代码片段间一起执行

二、乐观锁

相对悲观锁,一种对程序资源效率方面比较友好的一种锁机制,也是一种锁的思想,在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做,主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 Compare and Swap ( CAS )。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

先举个栗子:

SELECT quantity FROM items WHERE id = 1; --查询出商品库存信息,结果quantity = 3

UPDATE items SET quantity WHERE id = 1 AND quantity = 3; --修改商品库存信息为2

以上,在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

但是以上更新语句存在一个比较重要的问题,即传说中的ABA问题

比如说一个线程one从数据库中取出库存数3,这时候另一个线程two也从数据库中取出库存数3,并且two进行了一些操作变成了2,然后two又将库存数变成3,这时候线程one进行CAS操作发现数据库中仍然是3,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

version方式:

还有一个比较好的办法可以解决ABA问题,那就是通过一个单独的可以顺序递增的version字段,比较像svn的版本控制。

乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

以上SQL其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的粒度的。有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:

UPDATE items SET quantity = quantity -1 WHERE id = 1 AND quantity > 0; --修改商品库存

以上SQL语句中,如果用户下单数为1,则通过quantity - 1 > 0的方式进行乐观锁控制,update语句,在执行过程中,会在一次原子操作中自己查询一遍quantity的值,并将其扣减掉1。

上面这个栗子只是用简单的sql比较灵活的实现乐观锁的一种方式,对于java对CAS技术的支持,在网上搜了一下比较好的帖子传送门:https://www.cnblogs.com/qjjazry/p/6581568.html

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据库悲观锁乐观锁是两种不同的锁机制,应用场景和原理也不同。 1. 悲观锁 悲观锁是指在操作数据时,认为数据很可能会被其他线程修改,因此在操作前先加锁,操作完成后再释放锁,以保证在整个操作过程中,数据不会被其他线程修改。悲观锁的实现方式通常是通过数据库的锁机制来实现的,如行级锁、表级锁等。 应用场景: - 针对高并发的场景,比如银行转账、抢购等业务,为了避免数据的脏读、幻读等问题,需要使用悲观锁来确保数据的一致性。 - 在需要更新大量数据的情况下,为了避免其他线程修改数据,可以使用悲观锁来保证数据的完整性。 2. 乐观锁 乐观锁是指在操作数据时,认为数据很少会被其他线程修改,因此在操作前不加锁,而是在操作完成时检查数据是否被其他线程修改,如果没有修改,则提交操作,否则回滚操作。乐观锁的实现方式通常是通过版本控制来实现的,每次更新数据时都会将版本号加一,如果更新时发现版本号不一致,则说明数据已经被其他线程修改,需要回滚操作。 应用场景: - 针对并发量不高的场景,比如论坛帖子的编辑、评论等业务,可以使用乐观锁来提高并发性能。 - 在需要保证数据的一致性,但是数据的修改频率较低的情况下,也可以使用乐观锁来保证数据的完整性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值