4、乐观锁和CAS

乐观锁和CAS

一、乐观锁

1.1按照线程进入临界区是否锁住同步资源:悲观锁和乐观锁

悲观锁

​ 悲观锁:悲观的思想,每次进入临界区操作数据时,都会认为有别的线程会修改,所以线程每次读写数据时都会上锁,锁住同步资源,这样其他的线程需要读写这个数据时就会被阻塞,一直到拿到锁

Java的synchronize锁(升级为重量级锁,Monitor锁)也是悲观锁,

Innodb的行锁也属于悲观锁,行锁机制跟Innodb事务有关,事务执行完毕必须提交或回滚,否则导致其他事务一直阻塞。

乐观锁

​ 乐观锁:乐观的思想,每次拿数据时都会认为别的线程不会修改,所以不会上锁,**但在更新的时候会判断一下此期间别的线程有没有去更新这个数据。**采取在写时先读取当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样就更新),如果失败则重复读-比较-写的操作(自旋)。总之运用于读大于写的场景

@GetMapping("/optimisticLockUser")
public String optimisticLock(UserEntity userEntity) {
    Long userId = userEntity.getUserId();
    // 标记该线程是否修改成功
    Integer resultCount = 0;
    //失败重试,通过while循环
    while (resultCount <= 0) {
        // 1.根据userid 查找到对应的VERION版本号码 获取当前数据的版本号码
        UserEntity dbUserEntity = userMapper.selectById(userId);
        if (dbUserEntity == null) {
            return "未查询到该用户";
        }
        // 2.做update操作的时候,放入该版本号码  乐观锁
        userEntity.setVersion(dbUserEntity.getVersion());
        //UPDATE meite_user SET user_name=?, version=? WHERE user_id=? AND version=? AND deleted=0 所进行的操作
        resultCount = userMapper.updateById(userEntity);
    }
    return resultCount > 0 ? "success" : "fail";
}

Java中的乐观锁普遍通过CAS实现。

二、乐观锁底层JUC.CAS

什么是CAS

JDK5 所增加的 JUC(java.util.concurrent)并发包,对操作系统的底层 CAS 原子操作进行了封装,为上层 Java 程序提供了 CAS 操作的 API。

CAS 是在用户态完成,且CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少

CAS: Compare and Swap, 比较并交换。 底层会执行CAS(V,E,N)

CAS的三个操作数,修改目标的真实V,旧的预期值E,要修改的新值N,

当且仅当预期值E和目标的真实值V相等时,才能实现修改操作,即将V修改为N,否则什么也不做

2.1 Synchronize锁的缺点

在JDK1.5之前 的synchronized锁,线程没有获取synchronized锁,会直接阻塞,即synchronized锁会直接升级为重量级锁,后期唤醒的成本非常高

Jdk1.5及之后引入了synchronized锁的锁升级:偏向锁–>轻量级锁–>重量级锁

轻量级锁为(共享锁)乐观锁,被阻塞的线程也只处于用户态

但一直处于用户态(会一直占用CPU,消耗CPU资源)

而重量级锁属于(互斥锁)悲观锁,线程获取锁的过程,会进行用户态和内核态的切换

2.2 内核角度分析 轻量级/重量级锁的区别

用户态和内核态的区别

  • 内核态(Kernel Mode):运行操作系统程序,操作硬件 (内核空间)
  • 用户态:运行用户程序(用户空间)

为了安全,应用程序无法直接调用 硬件的功能。当应用程序需要硬件功能时 (如文件读写),就需要系统调用(当进程进行系统调用后,就会从用户态转换为内核态)

请添加图片描述

  • 何为零拷贝:零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

2.3 Linux内核互斥锁

Linux内核互斥锁mutex lock:

1、atomic_t count; //指示互斥锁的状态:

1:没有上锁,可以获得;

0:被锁定,不能获得。

负数:被锁定,且可能在该锁上有等待进程 ,初始化为没有上锁。

2、spinlock_t wait_lock; //等待获取互斥锁中使用的自旋锁。在获取互斥锁的过程中,操作会在自旋锁的保护中进行。初始化为为锁定。

3、struct list_head wait_list; //等待互斥锁的进程队列。

2.4Cpu上下文切换的原因

何为CPU上下文切换

Cpu通过分配时间片来执行任务,一个CPU在同一时刻只能运行一个线程,当一个任务的时间片用完(时间片耗尽或出现阻塞等情况),CPU会转去执行另一个线程,切换另一个任务,切换之前会保存上一个任务的状态,当下次在重新切换到该任务,就会继续切换之前的状态运行。任务从保存到再加载的过程就是一次上下文切换

上下文切换只发生在内核态中。内核态是CPU的一种特权模式,这种模式下,内核运行并且可以访问所有内存和其他系统资源。其他的程序,如应用程序,在最开始都是运行在用户态,但是他们能通过系统调用(在Java中可以通过navtive进行系统调用)来运行部分内核的代码

1.通过调用下列方法会导致自发性上下文切换:

Thread.sleep()

Object.wait()

Thread.yeild()

Thread.join()

LockSupport.park()

2.发生下列情况可能导致非自发性上下文切换:

  • 切出的线程时间片用完
  • 有优先级更高的线程被允许
  • JVM的垃圾回收(GC作为守护线程)

2.5如何避免上下文切换

  1. 无锁并发编程。
  2. CAS算法,JUC的Atomic包使用CAS算法来更新数据,而不需要加锁,可能会消耗cpu资源。
  3. 使用最少的线程,避免创建不必要的线程
  4. 协程:在单线程里实现多任务的调度,并在多线程里维护多个任务间的切换

关于协程:比线程更轻量级的存在,协程之于线程,就像线程之于进程

重要的是:协程不被操作系统内核所管理,而完全有应用程序所控制(只在用户态中运行,不会发生CPU上下文切换),从而减少了像线程切换那样消耗资源

2.6 重量级锁效率为什么低

轻量级都是在用户态完成;重量级需要用户态与内核态切换;
原因:重量级锁需要通过操作系统自身的互斥量(mutex lock,即互斥锁)来实现,然而这种实现方式需要通过用户态和核心态的切换来实现,但这个切换的过程会带来很大的性能开销

​ 申请锁时,会从用户态进入内核态,申请到后从内核态返回到用户态(两次切换);没有申请到时阻塞睡眠在内核态。使用完资源到释放锁,从用户态进入内核态,唤醒阻塞等待锁的进程,然后再返回到用户态(又两次切换);被唤醒进程在内核态申请到锁,返回用户态(可能其他申请锁的进程又要阻塞)。所以,使用一次锁,包括申请,持有到释放,当前进程要进行四次用户态与内核态的切换。同时,其他竞争锁的进程在这个过程中也要进行一次切换

2.7 CAS底层实现原理

Unsafe类中的CAS方法

Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全的底层操作,

如直接访问系统内存资源、自主管理内存资源等,Unsafe 大量的方法都是 native 方法,基于 C++

语言实现,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的

作用。

  • 此外操作系统层面的CAS是一条CPU的原子指令,所以使用CAS操作数据是不会造成数据的不一致问题。

完成 Java 应用层的 CAS 操作,主要涉及到的 Unsafe 方法调用,具体如下:

(1)获取 Unsafe 实例。

(2)调用 Unsafe 提供的 CAS 方法,这些方法主要封装了底层 CPU 的 CAS 原子操作。

(3) 调用 Unsafe 提供的字段偏移量方法,这些方法用于获取对象中的字段(属性)偏移量,此偏移量值需要作为参数提供给 CAS 操作。

请添加图片描述

请添加图片描述

请添加图片描述

CPU飙高的问题->CAS进行失败重试进入循环

var1对象的地址+var2(属性偏移量) ==对象当前属性的值

var5预期值,与上面的值进行判断,相等则替换原有的值,否则进行失败重试

private volatile int value;//volatile关键字修饰 具有可见性
    
 //value 属性值的地址偏移量
 private static final long valueOffset; (该偏移量再 加载原子类的时候通过static代码块获取)
 
     static {
 			try {
 				//计算 value 属性值的地址偏移量
				 valueOffset = unsafe.objectFieldOffset(
				 AtomicInteger.class.getDeclaredField("value"));
				 } catch (Exception ex) { throw new Error(ex); }
 			}
  • volatile的可见性,使其呈现出轻量级 同步机制的特性,其修饰的的变量在JVM的主存的值,具有可见性。即在多线程环境下,多个线程对这个变量进行修改,线程A在自己内存中将值修改后同步到主存,此时线程B会察觉到主存中值的改变,线程B会强制重新读取主存中的值
    • 但volatile不能保证原子性,但保证了线程安全性问题
 其实a++操作分为3步;
 从主内存中读取值  (全局共享变量V,位于主存之中,将其读取到线程私有内存中)
 进行值运算(加一操作)(对V进行修改变为N)
 将栈中副本中的新值同步到共享内存中 (再将其重新放回到主存中)
  • 原子性 是由Unsafe解决,从硬件层面保证原子性

2.8 CAS解决ABA问题

  • 何为ABA问题
ABA问题不仅存在于多线程,而且也来自于Mysql使用乐观锁
	
	eg:说一个线程 A 从内存位置 M 中取出 V1,另一个线程 B 也取出 V1。现在假设线程 B 进行了一些操作之后将 M 位置的数据 V1 变成了 V2,然后又在一些操作之后将 V2 变成 V1。之后,线程 A 进行 CAS 操作,但是线程 A 发现 M 位置的数据仍然是 V1,然后线程 A 操作成功。
	尽管线程 A 的 CAS 操作成功,但是不代表这个过程是没有问题的,线程 A 操作的数据 V1 可能已经不是之前的 V1,而是被线程 B 替换过的 V1,这就是ABA 问题。

ABA问题解决的方案, 尽量结合业务

  • 添加版本号
	很多乐观锁的实现,都是使用版本号(version)方式解决ABA问题。乐观锁每次执行数据的修改操作,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作 同时版本号加1,否则就执行失败。
  • 此外,原子类的AtomicStampReference 在CAS的基础上增加 了一个 印戳的标记了,用于觉察数据是否发生变化
public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp)
    	//这不是c++ map的元素吗
    }

private volatile Pair<V> pair; //通过可见性保证线程安全
//AtomicStampReference几个常用的方法
//获取被封装的数据
public V getRerference();
//获取被封装的数据的版本印戳
public int getStamp();
AtomicStampedReference 的 CAS 操作的定义如下:
public boolean compareAndSet(
 V expectedReference,//预期引用值
 V newReference, //更新后的引用值
 int expectedStamp, //预期印戳(Stamp)标志值
int newStamp) //更新后的印戳(Stamp)标志值
  • 使用 AtomicMarkableReference 解决 ABA 问题
    • 可以使用构造方法更新一个布尔类型的标记位和引用类型。就是AtomicStampReference的简化版

2.9 JUC原子类

**1.原子更新基本类型类 **

AtomicBoolean: 原子更新布尔类型。

AtomicInteger: 原子更新整型。

AtomicLong: 原子更新长整型。

2.原子更新数组

AtomicIntegerArray: 原子更新整型数组里的元素。

AtomicLongArray: 原子更新长整型数组里的元素。

AtomicReferenceArray: 原子更新引用类型数组里的元素。

3.原子更新引用类型

AtomicReference: 原子更新引用类型。

AtomicReferenceFieldUpdater: 原子更新引用类型的字段。

AtomicStampReference: 带Stamp印戳或标记),用于觉察数据是否发生变化,给数据带上一种时效性的检验

AtomicMarkableReferce: 原子更新带有标记位的引用类型,可以使用构造方法更新一个布尔类型的标记位和引用类型。

4.原子更新字段类

AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。

AtomicLongFieldUpdater: 原子更新长整型字段的更新器。

AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。

JDK8新增原子类

DoubleAccumulator

LongAccumulator

DoubleAdder

LongAdder(空间换时间,热点分离,将value分离成为一个数组,当多线程访问时,通过hash算法将线程映射到数组的一个元素进行CAS操作,和ConcurrentHashMap有异曲同工之妙)

LongAdder通过热点分离的思想,通过增加元素的个数,降低value的热点,使AtomicLong中的恶性CAS空自旋得以解决

2.10 CAS操作的弊端

主要有以下三点:

1、ABA问题 (Markable Stamped解决)
2、只能保证一个共享变量之间的原子性操作,当然AtomicReference类可以解决
3、开销问题

CAS恶性自旋:解决就是空间换时间

1、使用分散热点:使用LongAdder替代AtomicLong,线程映射到数组的一个元素进行CAS操作,和ConcurrentHashMap有异曲同工之妙

LongAdder通过热点分离的思想,通过增加元素的个数,降低value的热点,使AtomicLong中的恶性CAS空自旋得以解决

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值