一、概念

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

  • 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

二、实现方式

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。

乐观锁的实现方式主要有两种:CAS机制和版本号机制

1、CAS(Compare And Swap)

CAS操作包括了3个操作数:

  • 需要读写的内存位置(V)
  • 进行比较的预期值(A)
  • 拟写入的新值(B)

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

2、版本号机制

除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。

三、优缺点和适用场景

1、功能限制

与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

2、竞争激烈程度

如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

  • 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
  • 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

四、项目使用实例

1、单例模式使用悲观锁:双重校验锁实现对象单例(线程安全)


public class  
 Singleton  
 { 
 
private volatile static  
 Singleton uniqueInstance 
 ; 
 
private  
 Singleton 
 () { 
 
}
 
public static  
 Singleton getUniqueInstance 
 () { 
 
// 
 先判断对象是否已经实例过,没有实例化过才进入加锁代码
 
if  
  ( 
  uniqueInstance  
  ==  
  null 
  ) { 
 
 
// 
  类对象加锁 
 
 
synchronized  
  ( 
  Singleton 
  . 
  class 
  ) { 
 
 
if  
  ( 
  uniqueInstance  
  ==  
  null 
  ) { 
 
 
uniqueInstance  
  =  
  new  
  Singleton 
  (); 
 
 
} 
 
 
} 
 
 
}
 
 
return  
  uniqueInstance 
  ; 
 
 
} 
 
 
}


另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。


uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分


为三步执行:


1. 为 uniqueInstance 分配内存空间


2. 初始化 uniqueInstance


3. 将 uniqueInstance 指向分配的内存地址


但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2 。指令重排在单线程环境下不会出先问题,但是在


多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3 ,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance ,但此时 uniqueInstance 还未被 初始化。


使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。


2、乐观锁使用


sql中使用,例如springboot+mybatis操作数据库更新某个字段


update 'name' value ('xiaoming') where id =1 

 

  修改为 update 'name' value ('xiaoming') where id =1 and name='xiaohong'


此处修改则是CAS操作,防止数据被多次修改,先取出name值,再判断是否已被修改。