关于锁的知识的总结

简介

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

  • 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

在说明实现方式之前,需要明确:乐观锁和悲观锁是两种思想,它们的使用是非常广泛的,不局限于某种编程语言或数据库。

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。

乐观锁的实现方式主要有两种:CAS机制和版本号机制。

CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

功能限制

与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

CAS问题:ABA问题、高竞争下的开销问题、功能限制

2、竞争激烈程度

如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

  • 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
  • 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

1.数据库中的行锁,表锁,读锁,写锁以及syncronized实现的锁,都是悲观锁
2.innodb默认使用行锁,而行锁是基于索引的,因此要想加上行锁,在加锁时必须命中索引,否则将使用标锁。
3.乐观锁通过在表中增加一个版本号或时间戳来实现,其中,版本最常见。
4.乐观锁的原理:事务在从数据库取数据是,会将该数据的版本也取出来(v1),当事务对数据变动完毕,想要将其更新到表中时,会将之前取出的版本号v1与数据中最新的版本号v2进行比较,如果v1=v2,那么说明在此事务期间,没有其他事务对数据进行修改,此时,就允许事务对表中的数据进行修改,并且修改时版本号会加1,以此表明数据已被改动。如果v1!=v2,那么说明事务操作过程中,有其它事务对数据进行了修改,此时,一般的处理办法是通知用户,让用户重新操作。所以,乐观锁需要人为控制,而悲观锁不需要。

比较:
悲观锁:一个事务用悲观锁对数据加锁之后,其它事务将不能对加锁的数据进行除查询之外的任何操作,影响了系统的吞吐量。适合写多的场景。
乐观锁:不在数据库上加锁,任何事务都可以对数据进行操作,在更新时才进行校验,提高吞吐量。适合读多的场景。

重量级锁:拿不到该锁后,里面进入阻塞模式的锁。(线程进入阻塞状态是比较耗时的,需要,保存线程执行状态,上下文等数据,还涉及到用户态到内核态的转换,同时,线程从阻塞态唤醒也是比较耗时的)

自旋锁:如果拿不到锁,不会马上进入阻塞状态,而是等待一段时间(类似于线程在那里做空循环),如果循环一定的次数,还是拿不到锁,那么它立即进入阻塞状态。

自适应自旋锁:普通的自旋锁每个线程循环等待的时间是一样的,由用户指定。而自适应自旋锁本身能够判断需要循环的次数,而且不同线程可能的循环次数也可能不一样。其原理是:如果一个线程在前不久拿到过这个锁,或者它之前经常拿到这个锁,那么我们可以认为,它再次拿到这个锁的概率非常大,所以循环次数会多一些。反之,则循环次数少一些。

上面这三种锁,都是悲观锁

轻量级锁
进入的时候,不加锁,只需要做一个状态标记就好。如果未被其它线程标记,则进入执行。采用CAS来改变状态比加锁花销小很多。如果遇到有竞争,则将轻量级锁升级为重量级锁。

偏向锁
如果这个方法没人进来过,那么一个线程首次进入某个方法时,会采用CAS机制加标记,并会把线程ID也记录进去。让后线程退出时,不改变这个标记(它认为除了自己外,其它线程不会执行这个方法)。然后,线程下次进入这个方法的时候,如果,标记的线程ID是自己的,那么它就直接进入这个方法执行。如果线程ID不是自己的,则将偏向锁升级为轻量级锁。

上面这两种锁是乐观锁

共享锁:也称读锁,允许多个连接在同一时刻并发的读取同一资源,互不干扰。
排它锁:也称写锁。一个写锁会阻塞其它写锁或读锁,防止其它用户对该数据的读写。