乐观锁与悲观锁如何理解?

乐观锁

就是乐观地认为每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间有没有人去更新这个数据,可以使用版本号等机制。

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁,在Java中java.util.concurrent.atomic包下的原子变量类就是使用了一种乐观锁的实现方式CAS实现的。

乐观锁的实现方式

版本号机制

使用版本标识来确定读到的数据与提交时的数据是否一致,提交后修改版本标识,不一致时可以采用丢弃和再次尝试。

CAS

所谓的CAS即为Compare and Swap,即当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程被告知本次竞争失败并可以重新竞争或者挂起。

CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS操作中包含三个操作数—需要读写的内存位置V,进行比较的预期原值A,和拟写入新的值B.如果内存位置V的值与预期的原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。

CAS产生的问题

  • ABA问题:

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A就会变成1A - 2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类 AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  • 循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
我们可以用等红绿灯作为例子。Java 线程的阻塞相当于熄火停车,而自旋状态相当于怠速停车。如果红灯的等待时间非常长,那么熄火停车相对省油一些;如果红灯的等待时间非常短,比如我们在同步代码块中只做了一个整型加法,那么在短时间内锁肯定会被释放出来,因此怠速停车更合适。然而,对于JVM来说,它并不能看到红灯的剩余时间,也就没法根据等待时间的长短来选择是自旋还是阻塞。JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环数)。就我们的例子来说,如果之前不熄火等待了绿灯,那么这次不熄火的时间就长一点;如果之前不熄火没等待绿灯,那么这次不熄火的时间就短一点。

  • 公平性

自旋状态还带来另外一个副作用,不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。 然而,处于自旋状态的线程,则很有可能优先获得这把锁

  • 只能保证一个共享变量的原子操作。

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的关系型数据库里边就用了这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁,再比如java里面的synchronized关键字的实现夜视悲观锁

猜你喜欢

转载自blog.csdn.net/qq_41552331/article/details/106821032