乐观锁:总是认为不会产生并发问题,每次读取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新数据时,会判断其他线程在这之前有没有对数据进行修改,如果其他线程对数据进行了修改,因为并发冲突而导致更新失败,该线程就会反复重试更新数据,直到成功为止。
悲观锁:总是假设最坏的情况,每次读取数据时都认为其他线程会修改数据,所以都会加锁(如读锁、写锁、行锁、表锁等),当其他线程想要访问数据时,都会被阻塞挂起。
乐观锁实现方式:cas、volatile,悲观锁实现方式:synchronized、Lock
在Java中,java.util.concurrent.atomic包下面的原子变量类就是使用了CAS方式实现的。
悲观锁机制存在以下问题:
1. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
2. 一个线程持有锁会导致其它所有需要此锁的线程挂起。
3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
乐观锁的具体实现细节主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 Compare and Swap ( CAS )。
CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败。CAS技术通常与循环结合使用,形成自旋锁,这样可以使得更新失败的线程再次尝试。
CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。
在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。
以java.util.concurrent.atomic包中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解 getAndIncrement 方法,该方法的作用相当于 ++i 操作。getAndIncrement 采用了CAS操作,每次从内存中读取数据,然后将此数据与此数据+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
public final int getAndIncrement() {
for (;;) { //死循环,与CAS结合形成自旋锁
int current = get(); //获取旧值
int next = current + 1; //待替换的新值
if (compareAndSet(current, next)) //CAS操作,成功则返回旧值
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
在没有锁的机制下,字段value要借助volatile原语,保证线程间的数据是可见性,这样在获取变量的值的时候才能直接读取。
compareAndSet 利用JNI(Java Native Interface)compareAndSwapInt来完成CPU指令的操作。其中本地方法unsafe.compareAndSwapInt(this, valueOffset, expect, update)类似如下逻辑:
if (this.valueOffset.value == expect) {
this.valueOffset.value = update;
return true;
} else {
return false;
}
使用version方式实现乐观锁:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。核心SQL代码如下所示:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};