乐观锁和悲观锁

悲观锁

顾名思义,悲观锁总是觉得共享资源每次访问都可能会出问题,所以它在每次访问共享共享变量时都会申请锁,等待上一个锁的持有者释放之后再访问变量。保证同一时刻只有一个线程使用。
java中的Synchronized和ReentrantLock等独占锁就是悲观锁实现

public void performSynchronisedTask(){
	sychronized(this){
		// Operations that need to be synchronized
	}	
}
private Lock lock = new ReentrantLock();
lock.lock();
try{
	// Operations that need to be synchronized
}finally{
	lock.unlock();
}

很容易看出来,悲观锁是很容易造成线程阻塞的,大量的解锁和加锁的操作会造成额外的性能开销。甚至可能引发死锁问题。

乐观锁

与悲观锁相反,乐观锁总是认为访问共享资源不会出现问题,他就只在提交最终结果的时候检查该资源有没有被其他线程更改,具体检查的方法有CAS算法和版本号机制。
java中的AtomicInterger和LongAdder就是用的乐观锁想法

// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
// 代价就是会消耗更多的内存空间(空间换时间)
LongAdder sum = new LongAdder();
sum.increment();

乐观锁和悲观锁相比,不存在锁竞争,不会有死锁问题,但是如果冲突很多的情况下,使用乐观锁可能会产生频繁的失败重试,这非常影响性能

版本号机制

在数据表上加一个表示版本号的version字段,在读取数据时将version也顺便读出。在写回数据时,先比较之前读出的version和该数据表当下的version值是否一致,如果不一致说明操作出现冲突,一致的话就提交更新,并将version值加一。比如:

线程A 线程B
读取账户表及version = 1
账户余额+100 读取账户表及version = 1
提交修改,此时version仍为1,修改成功,version = 2 余额-100
提交修改,此时version = 2。修改被驳回

CAS算法

CAS即compare and swap,其思想就是在提交更新的时候用一个预期值和要更新的变量值进行比较,相等的话才更新。
CAS是一个原子操作,底层依赖于CPU的原子指令。
CAS涉及三个操作数:Value:要更新的变量值,Expected:预期值,New:要更新的新值
CAS工作过程如下:

线程A 线程B
读取变量i = 1
执行i+5,此时V = 1,E = 1,N = 6 读取变量i= 1
提交更新,因为i == E,成功 i + 9,此时V = 1,E = 1,N = 10
提交更新,i != E,提交驳回

乐观锁可能存在的问题

ABA问题

即使在提交更新时,预期值和要更新的变量值相等,也不一定就没有被其它线程修改,可能从A到B,又从B修改为A,即ABA问题。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK1.5以后的AtomicStampedReference类就是用来解决 ABA 问题的,其中的compareAndSet()方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

循环时间长开销大CAS

经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:

  • 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
  • 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。

只能保证一个共享变量的原子操作(1.5之前)

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

你可能感兴趣的:(java基础知识,java,算法,开发语言)