天天看点

乐观锁和悲观锁原理和应用一、悲观锁二、乐观锁

一、悲观锁

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

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

继续阅读