这是我参与更文挑战的第22天,活动详情查看:更文挑战
乐观锁
乐观锁 是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改,所以没有上锁。它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
这里举一个场景例子:在线文档。
我们都知道在线文档可以同时多人编辑的,如果使用了悲观锁,那么只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档了,这用户体验当然不好了。那实现多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。
怎么样才算发生冲突?这里举个例子,比如用户 A 先在浏览器编辑文档,之后用户 B 在浏览器也打开了相同的文档进行编辑,但是用户 B 比用户 A 提交改动,这一过程用户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并行修改的地方就会发生冲突。
服务端要怎么验证是否冲突了呢?通常方案如下:
- 由于发生冲突的概率比较低,所以先让用户编辑文档,但是浏览器在下载文档时会记录下服务端返回的文档版本号;
- 当用户提交修改时,发给服务端的请求会带上原始文档版本号,服务器收到后将它与当前版本号进行比较,如果版本号一致则修改成功,否则提交失败。
实际上,我们常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。
Java中的乐观锁: CAS,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。
悲观锁
悲观锁做事比较悲观,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会修改,所以每次读写数据都会认为其他线程会修改,所以每次读写数据时都会上锁。其他线程想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程获取到锁。
CAS
CAS:全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。所以 CAS 是一个乐观锁。
CAS 的应用场景:
CAS 的使用能够避免线程的阻塞,如自旋锁的实现。
多数情况下我们使用的是 while true 直到成功为止。
CAS 缺点
ABA 的问题,就是一个值从A变成了B又变成了A,使用CAS操作不能发现这个值发生变化了,处理方式是可以使用携带类似时间戳的版本AtomicStampedReference
性能问题,我们使用时大部分时间使用的是 while true 方式对数据的修改,直到成功为止。优势就是相应极快,但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。