1、乐观锁
(1)乐观锁的概念
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
(2)乐观锁的应用
乐观锁适用于多读的应用类型(写比较少的情况),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能。像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
(3)乐观锁的实现方式
1)版本号机制(version)
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
2)CAS算法
CAS(Compare and Swap),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B;如果内存位置V的值与预期原值A不匹配,处理器则返回该位置的值。一般情况下是一个自旋操作,即不断的重试。
(4)CAS的缺点
1) ABA 问题
如果一个线程t1从内存位置V中取出变量A,此时另一个线程t2也从内存中取出变量A,并且t2进行了一些操作将A变成了B,然后t2又将V位置的B改回了A,这时候线程t1进行CAS操作发现内存中仍然是A,然后t1操作成功。线程t1的CAS操作成功,CAS操作误认为该值从来没有被修改过。这个问题被称为CAS操作的 “ABA”问题。
2)循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3)只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作;但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这种情况可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。
2、悲观锁
(1)悲观锁的概念
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
(2)悲观锁的缺点
1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
4)容易造成死锁。