乐观锁与悲观锁、自旋锁

做~自己de王妃 提交于 2020-03-11 03:33:08

乐观锁

乐观锁是一种乐观的思想,即认为读多写少,遇到并发的可能性低,每次拿数据时都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。

Java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁就是悲观的思想,即认为写多,遇到并发的可能性高,每次拿数据时,都会认为别人会修改数据,所以在每次读数据的时候都会上锁,这样当别人想读写这个数据时就会阻塞,直到拿到锁。(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

Java 中的悲观锁就是 Synchronized,AQS 框架下的锁则是先尝试 CAS 乐观锁去获取锁,获取不到,才会转换为悲观锁,如 ReentrantLock。

乐观锁的缺点

  1. ABA 问题
    CAS 会导致 “ABA 问题”。CAS 算法实现的一个重要前提是需要取出内存中某时刻的数据,而在下一时刻比较并替换,那么在这个时间差会导致数据的变化。

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

    部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败,因为每次操作的版本号都会随着增加,所有不会出现 ABA 的问题。

  2. 只能保证一个共享变量的原子操作
    CAS 只对单个变量有效,但涉及到多个共享变量时 CAS 无效。

两种锁的使用场景

这两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于读多写少的情况,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吐吞量。但如果是多写的情况下,一般会经常产生冲突,当获取锁失败时,就要不断的进行自旋操作以尝试重新获取锁,这样就会导致性能下降,所以一般多写的场景下用悲观锁就比较合适。

悲观锁和乐观锁并非是一种实际的锁,而是指一种加锁的概念

自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁资源的线程就不需要做内核态与用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等待持有锁的线程释放锁后就可以立即获取锁,这样就避免用户态与内核态的切换消耗。

线程自旋是需要消耗 CPU 的,说白了就是让 CPU 在做无用功,如果一直获取不到锁,那线程也不能一直占用 CPU 自旋做无用功,所以需要设定一个自旋等待的最大时间。

如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作消耗,这些操作会导致线程发生两次上下文切换!

但是如果锁的竞争激烈,或持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 CPU 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞的消耗,其它需要 CPU 的线程又不能获取到 CPU,造成 CPU 的浪费。所以这种情况下我们要关闭自旋锁。

自适应自旋锁

自旋的次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么 JVM 认为该锁自旋获取到锁的可能性很大,会自动增加自旋的时间,比如增加到 50 次循环,相反对于某个锁,自旋很少成功获取到锁,那以后获取锁时,将可能省略掉自旋过程,以避免浪费 CPU 资源。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!