【多线程】锁策略

🏀🏀🏀来都来了,不妨点个关注!
🎧🎧🎧博客主页:欢迎各位大佬!
在这里插入图片描述

1. 乐观锁和悲观锁

1.1 基本概念

  • 乐观锁:乐观锁在操作数据时比较乐观,认为别人不会修改数据,因此就不会进行加锁。只是在执行更新操作的时候会进行判断别人是否修改了数据,是则放弃操作,否则继续操作。
  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人同时会修改数据,因此在操作数据的时候会进行加锁,直到操作完成才释放锁,在操作数据期间其他人不能修改数据。

1.2 实现方式

需要注意的是,这里的锁策略是一种思想,它的用途非常广泛,并不是某一种编程语言或数据库特有的。

  • 悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。
  • 乐观锁的实现方式主要有两种:CAS机制和版本号机制

CAS机制:CAS的全称是compare and swap(比较和交换),被广泛应用于各大框架中,它的实现思想比较简单,就是用一个预期值和要更新的实际值进行比较,相等则进行更新操作。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
CAS 涉及到三个操作数:

  • V:要更新的变量值(Var)
  • E:预期值(Expected)
  • N:拟写入的新值(New)

这个和我们之前对i++操作进行加锁让它变成一个原子操作类似。
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
举一个例子:线程A要修改变量i的值为6,i此时的值为1(V=1,E=1,N=6,假设没有ABA问题)

  1. i与E=1进行比较,如果相等,说明没有被其他线程修改,进行更新操作V=6。
  2. i与E=1进行比较,如果不相等,说明数据已经被其他线程修改,当前线程放弃更新,CAS操作失败。

ABA问题:
如果一个变量V初次读取到的值为A,并且在准备赋值的时候读取到变量V的值依然为A,那我们就能认为它没有被修改过吗,很显然是不可以的,变量V可能在这其中经历过一些修改,比如修改成B了最后又修改回了A,那CAS操作就会误以为它没有被修改过,这就是ABA问题。
解决方案: 在变量前面加上版本号或时间戳。

版本号机制:一般是在数据表中加一个数据版本号字段(version),表示数据被修改的次数,当数据被修改时就进行加一操作。当线程A进行更新数据时同时会读取version,进行提交时会对比此时的version值和之前的读取到的version值是否相同,相同则进行更新操作,否则重试更新操作,直到更新成功。
例子:假设数据库中余额表中,有一个版本号字段version,当前用户余额money=100。
3. 此时操作A读取到version=1,并将用户余额更新为50(100-50)。
4. 在操作A进行更新的时候,操作B读取到version=1,并将用户余额更新为80(100-20)。
5. 操作A将version值和更新的账户余额50提交到数据库中进行更新,此时对比数据库中version和提交的version相等,更新成功,数据库中version更新为2。
6. 操作B将version值和更新的账户余额80提交到数据库中进行更新,此时对比数据库中的version为2,与提交的version不相等,放弃更新。

2. 轻量级锁和重量级锁

  • 轻量级锁:加锁解锁,更快更高效。
  • 重量级锁:加锁解锁,过程更慢更低效。

锁的核心特性“原子性”追根溯源是CPU这样的硬件设备提供的。

  • CPU提供原子操作指令
  • 操作系统基于CPU的原子指令,实现了mutex(互斥锁)
  • JVM基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类
    在这里插入图片描述
    注意:synchronized不仅仅是对mutex的封装,在synchronized内部还做了很多其他工作。

轻量级锁: 轻量级锁的加锁机制尽可能不使用mutex,从而减少内核态和用户态之间的切换,也减少了线程的调度问题。轻量级锁适用于锁竞争不激烈的场景,可以显著提高性能。(在使用mutex互斥锁之后会膨胀成重量级锁)
重量级锁: 重量级锁过度依赖操作系统提供的mutex(互斥锁),这种锁同步方式的消耗非常大,主要包括系统调度引起的用户态和内核态的切换,以及线程阻塞造成的线程调度问题。
通常情况下,悲观锁很可能是重量级锁,乐观锁很可能是轻量级锁。

synchronized最开始是轻量级锁,但如果锁冲突比较严重,就会变成重量级锁。

理解用户态和内核态:
比如现在小万肚子饿了,她有两种方式选择吃饭:

  1. 自己煮个泡面,方便快捷,这就类似于轻量级锁,时间成本是比较可控的。
  2. 打开外卖软件点外卖,等外卖到即可,这就类似于重量级锁,外卖到达的时间是不太可控的,可能他现在手里单比较多优先考虑送别人的,那小万就得苦苦等着。(就如小万之前点过一次外卖等了两个小时,最后气的小万取消订单,老老实实的去煮泡面了)

3.自旋锁和挂起等待锁

  • 自旋锁:自旋锁是轻量级锁的一种典型实现,在加锁过程中,自旋锁会一直判断锁是否被占用,这个过程消耗了更多的CPU资源,但一旦锁被释放就能第一时间获得锁,自旋锁适用于锁竞争不激烈且保持锁的时间短的场景。
  • 挂起等待锁:挂起等待锁是重量级锁的一种典型实现,当尝试加锁并失败,出现锁冲突时,就会让当前尝试加锁的线程被挂起(进入阻塞状态),当锁被释放后,系统才会去唤醒该线程去重新去尝试获取锁。挂起等待锁等待的时间更长,一旦线程被阻塞,就不知道何时会被唤醒了。

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的。

4.互斥锁和读写锁

  • 互斥锁:加锁就是单纯的加锁,没有再细分的操作了。
  • 读写锁:读写锁是一种特殊的锁,它将锁分为读锁和写锁。
    读写锁中约定:
  1. 读锁和读锁之间不会产生锁竞争,不会有阻塞等待
  2. 写锁和写锁之间会产生锁竞争
  3. 读锁和写锁之间会产生锁竞争
    适用场景:读写锁适用于读多写少的场景,可以显著提高并发性能。Java中的ReentrantReadWriteLock就是读写锁的一个实现。

synchronized就是互斥锁。

5.公平锁和不公平锁

  • 公平锁:严格按照先来后到的顺序来获取锁,哪个线程等待的时间长,哪个线程就拿到锁。
  • 非公平锁:若干个线程各凭本事,随机地获取到锁,和线程等待时间无关。

synchronized就是非公平锁。

6.不可重入锁和可重入锁

  • 可重入锁:允许同一个线程多次获取同一把锁,例如Java中的ReentrantLock和synchronized都是可重入锁。
  • 不可重入锁:一个线程在持有锁的情况下,如果再次尝试获取该锁,则会导致死锁。

synchronized是可重入锁。

本次分享就结束了,感谢支持!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值