首先,悲观锁与乐观锁是根据操作时是否锁住资源来判别的。悲观锁获取到锁时,必须要锁住资源;乐观锁则不会。一开始两线程争抢锁:
线程访问资源.jpg
悲观锁之所以悲观,那是因为它觉得如果不锁住这个资源,别的线程就会来争抢,造成数据结果错误,所以悲观锁为了确保结果的正确性,会在每次获取并修改数据时,都把数据锁住,让其他线程无法访问该数据,这样就可以确保数据内容万无一失,从这点看悲观锁特别稳。
下面通过几张图,大概就能明白悲观锁的执行过程了:
接上面场景,如果 A 拿到锁,正在操作资源,B 就只能进入等待。
线程 B,进入等待.jpg
直至 A 执行完毕释放锁,CPU 唤醒等待此锁的线程 B。
线程 A 执行完毕,释放锁.jpg
线程 B 获取到了锁,就可以对同步资源进行自己的操作。这就是悲观锁的操作流程。
线程 B 加锁成功,操作资源.jpg
乐观锁顾名思义,比较乐观。相比于悲观锁,它是不锁住资源的,因为它觉得自己在操作资源时并不会有其他线程干扰。
因此,为了保障数据的正确性。它在操作之前,会先判断在自己操作期间,其他线程是否有操作。如果没有,直接操作;如果有,则根据业务选择报错或者重试。
下面来看看,乐观锁的执行过程:
乐观锁的这把锁,其实就是依赖的 CAS (compare and swap:比较并交换)算法。所以,它在操作资源之前并不需要获得锁,直接读取资源到自己的工作内存内操作:
乐观锁获取数据直接操作.jpg
操作完成,准备更新资源时。就会触发 CAS 算法,判断资源是否被其他线程修改过。
乐观锁 CAS 判断.jpg
没有修改过,直接更新,线程执行完毕。
CAS 过程 1.jpg
被修改过,根据业务逻辑走下一步,是重试还是报错?
CAS 过程 2.jpg
值得注意的是,不管是在 Java 还是数据库中都用到了。悲观锁、乐观锁的概念,只是实现方式稍有不同。下面介绍下 Java 中的悲观、乐观锁:
这两够经典的,synchronized 必须要获取 mintor 锁才能进去操作资源;Lock 接口也是,必须显示调用 lock 才能操作资源。必须取到锁才能进行操作,这就是悲观锁的思想。
这类应该很常用,比如用作线程间的计数器。典型如 AtomicInteger 类在进行运算时,就使用了乐观锁的思想。使用 compareAndSet 方法更新数据,更新失败则重试。
数据库中的悲观、乐观锁:
比如以下的 update 语句:
UPDATE people
SET
name = '狗哥',
version = 2
WHERE id = 30624700
AND version = 1
说了这么久,悲观锁乐观锁的区别我知道了。那这两种锁在啥样的场景下使用呢?
有人说悲观锁比乐观锁消耗大,因为悲观要锁、乐观不要锁(注意,这里我说不要是实际没锁住资源,它的锁其实是 CAS 算法)。是的,如果并发量很小的情况下,悲观锁确实比乐观锁消耗大。但如果并发量很高,导致乐观锁一直在重试,这时它消耗的资源比固定开销的悲观大,也是说不定的。
-END-