悲观锁
悲观锁总是会假设最坏的情况,认为每次去拿数据的时候都会被修改。因此其总是会在拿数据的时候进行上锁。其他人会阻塞一直到其得到锁。 可以理解为充电器的插座和插头,同一时间一个插座只能由一个插头实现。 比较常见的悲观锁即synchronized和ReentrantLock。
乐观锁
乐观锁是假设最好的情况,每次去拿数据的时候都认为数据不会被别人修改,所以并不会对读取的数据进行上锁。但是在更新的时候回判断一下这个期间别人有没有去更新这个数据,这个可以使用版本号机制和CAS算法来实现。
乐观锁常适用于多读的类型,以便提高吞吐量。
常见的乐观锁有write_condition机制、juc包下的atomic包下面的原子变量类就是使用乐观锁CAS实现的。
乐观锁的实现方式
乐观锁常见的实现方式有版本号机制和CAS算法。
1、版本号机制
版本号机制,即在数据中加入一个版本号version字段。当数据被修改时,version值会加一。
举个简单的例子。
假如现在有两个线程A和B。有一个数据,其值为100,version为1。
- A线程对数据进行-50操作。先读取其version = 1,然后执行100 - 50 = 50.
- B线程对数据进行+20操作。先读取其version = 2,然后执行100 + 20 = 120.
- A线程完成操作,将version + 1 ,此时version为2。连同操作后的数据50一起提交到数据库进行更新。由于此时提交的version大于原有的version,因此原有的数据更新。数据库的version变为2.
- B线程也完成了操作,准备将version+1也写入数据,但此时发现,数据库中的version已经不比B线程的version小,不满足乐观锁的“提交版本必须大于当期版本才能执行更新”的乐观锁策略。 因此 B线程提交失败。
从而保证了A和B的数据不会出现错误。
2、CAS算法
CAS,即Compare and Swap(比较与交换),是一种无锁的算法。通过名字就可以知道,是先比较,然后再决定是否交换。这是一种能够在不适用锁的情况下实现同步的算法,也叫 非阻塞同步 。
CAS算法通常涉及到三个值:需要读写的内存值V、进行比较的值A和拟写入的新值B。 只有当V的值等于A的时候,CAS才能使用新值B来代替V的值,不然就会一直进行自旋。
乐观锁存在的问题
乐观锁 本身也会存在一定的局限。
1、ABA问题
如果说 事务第一次读取到的A值是20,那么当我们准备对A值进行修改,发现A值依然是20的时候,CAS会认为这个A值一直没有被修改过。那么在这个过程中A值真的没有被修改过吗?显然不能,因为很有可能A值已经被修改过,然后又被修改回A值。
这个问题便被称为 “ABA”问题。
2、循环时间长,开销大
CAS自旋如果长时间无法获取到锁,那么它就会一直循环执行,而这会给CPU带来巨大的执行开销。
3、只能保证一个共享变量的原子操作
CAS只能对单个共享变量有效。一旦涉及到多个共享变量时,CAS无法确保生效。
CAS与synchronized的场景
通常而言,CAS可以使用于写比较少的场景,而synchronized则是写比较多的场景。