背景
数据存在属性A。现在存在两个线程,查询到这行数据后对属性A修改。两个线程并发执行,就会存在线程看不到另一个线程对属性做出改变的行为。
解决方案:
1. 在访问数据的时候就加上锁,更新完成后释放锁,没有拿到锁的线程只能阻塞等待,因此,之后获取锁能够看到属性的修改,
这就是下面要说的悲观锁思想
2. 在更新数据的时候同时比较属性是否改变了,如果没有,就更新,否则,查询之后,在更新,这是乐观锁的一个思想。
悲观锁
总是假设最坏的情况,每次访问数据的时候,就对资源上锁,没有拿到锁的线程只能阻塞等待。
关系型数据库中的表锁,行锁,读锁,写锁以及java里面的synchronized和ReentrantLock锁都是悲观锁思想的体现。
乐观锁
总是假设最好的情况,认为每次访问数据的时候不会修改数据,不上锁,但是更新的时候进行比较看数据是否改变。
乐观锁的实现
使用版本号机制和CAS算法实现。
1. 版本号机制
在数据库中添加一个version字段来记录修改字段的变化,改变这个字段就对这个version加1。线程更新字段的时候,需要带上版本号进行更新,版本号不一致,说明已经被改过。
2. CAS算法
CAS算法,Compare And Set的缩写,涉及到三个值
- 字段的内存值 V
- 持有的值A
- 希望更新的值B
只有当V和A相等时候更新,一般是一个自旋操作,重试多次。
CAS算法的问题
1. ABA问题
一个线程将值修改后又改回原值,另一个线程看到的还是原值,就认为值没有发生过改变,这就是ABA问题
AtomicStampedReference类,版本号的引用类型,将引用和整数值进行绑定来解决ABA问题。
2. 自旋CAS循环开销大
如果自旋的操作长时间无法拿到锁,就会造成不必要的开销。
3. 只能对单个变量进行CAS操作,不过对于多个变量从jdk1.5开始提供了AtomicReference类,可以将涉及到CAS操作的变量封装成一个AtomicReference对象,执行CAS操作。
悲观锁和乐观锁的应用场景
乐观锁:适合于写少的场景,就算发生了多个线程同时对一个变量的写操作,也能避免使用锁,减少开销
悲观锁:适合于写多的场景,这种场景意味着冲突时常发生,如果使用乐观锁,多次自旋反而性能较低,此时用于悲观锁比较好。
补充
Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
转载: