龙空技术网

详解Java中的悲观锁与乐观锁

梅子抱福 136

前言:

现在你们对“悲观锁的实现方式java”大约比较注意,大家都需要了解一些“悲观锁的实现方式java”的相关资讯。那么小编同时在网摘上网罗了一些有关“悲观锁的实现方式java””的相关文章,希望看官们能喜欢,我们快快来学习一下吧!

一、概念

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

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

二、实现方式

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如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值,再判断是否已被修改。

标签: #悲观锁的实现方式java