多线程(常见的锁策略)

接下来讲解的锁策略不仅仅是局限于 Java . 任何和 “锁” 相关的话题, 都可能会涉及到以下内容. 这些特性主要是给锁的实现者来参考的.
普通的程序猿也需要了解一些, 对于合理的使用锁也是有很大帮助的.

一、乐观锁和悲观锁

1.1 乐观锁

假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并
发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
通俗来讲就是,预测接下来锁冲突的概率不大,进行对应的操作

1.2 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这 样别人想拿这个数据就会阻塞直到它拿到锁。
通俗来将是,预测接下来锁冲突的概率很大,进行对应的操作

【举例理解乐观锁和悲观锁】
场景:同学 A 和 同学 B 想请教老师一个问题.

同学 A 认为 “老师是比较忙的, 我来问问题, 老师不一定有空解答”. 因此同学 A 会先给老师发消息: “老师
你忙嘛? 我下午两点能来找你问个问题嘛?” (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题.
如果得到了否定的答复, 那就等一段时间, 下次再来和老师确定时间. 这个是悲观锁.
同学 B 认为 “老师是比较闲的, 我来问问题, 老师大概率是有空解答的”. 因此同学 B 直接就来找老师.(没
加锁, 直接访问资源) 如果老师确实比较闲, 那么直接问题就解决了. 如果老师这会确实很忙, 那么同学 B
也不会打扰老师, 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁.

这两种思路不能说谁优谁劣, 而是看当前的场景是否合适.
如果当前老师确实比较忙, 那么使用悲观锁的策略更合适, 使用乐观锁会导致 “白跑很多趟”, 耗费额
外的资源.
如果当前老师确实比较闲, 那么使用乐观锁的策略更合适, 使用悲观锁会让效率比较低.

synchronized即是一个乐观锁也是一个悲观锁,准确来说是一个自适应锁

  • 当锁冲突概率不大的时候,就以乐观锁的方式运行,往往是纯用户态执行的
  • 当发现锁冲突的概率大了,就以悲观锁的方式运行,往往要进入内核,对当前的线程进行挂起等待

就好比同学 C 开始认为 “老师比较闲的”, 问问题都会直接去找老师. 但是直接来找两次老师之后, 发现老师都挺忙的, 于是下次再来问问题, 就先发个消息问问老师忙不 忙, 再决定是否来问问题.

二、普通的互斥锁和读写锁

2.1 普通的互斥锁

synchronized就属于普通的互斥锁,两个加锁操作之间会发生竞争

2.2 读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需
要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

读写锁,则是将加锁操作细化了,加锁分成了"加读锁"和"加写锁"

一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.

  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
  • 两个线程都要写一个数据, 有线程安全问题.
  • 一个线程读另外一个线程写, 也有线程安全问题.

读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行
    加锁解锁.
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进
    行加锁解锁.

其中, 读加锁和读加锁之间, 不互斥.
写加锁和写加锁之间, 互斥.
读加锁和写加锁之间, 互斥

只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了. 因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径

读写锁特别适合于 “频繁读, 不频繁写” 的场景中.
例如:

三、重量级锁和轻量级锁

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

  • CPU 提供了 “原子操作指令”.
  • 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
  • JVM 基于操作系统提供的互斥锁, 实现了synchronized 和 ReentrantLock 等关键字和类.

    在这里插入图片描述


    注意, synchronized 并不仅仅是对 mutex 进行封装, 在 synchronized 内部还做了很多其他的
    工作

3.1 重量级锁

重量级锁:锁开销比较大,做的工作比较多, 加锁机制重度依赖了 OS 提供了 mutex

  • 大量的内核态用户态切换
  • 很容易引发线程的调度

这两个操作, 成本比较高. 一旦涉及到用户态和内核态的切换, 就意味着 “沧海桑田”.

悲观锁经常是重量级锁

3.2 轻量级锁

轻量级锁:锁开销比较下,做的工作比较少, 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex

  • 少量内核态用户态切换
  • 不太容易引发线程调度

乐观锁经常是轻量级锁

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.

四、自旋锁和挂起等待锁

4.1 自旋锁

自旋锁是轻量级锁的具体实现,按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度. 但实际上,
大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.当发现锁冲突的时候,不会挂起等待,会迅速在来尝试看锁能不能获取到

【自旋锁的伪代码】:

while (抢锁(lock) == 失败) {}

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会 在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.

自旋锁是轻量级锁,也是乐观锁

【自旋锁的优缺点】:

  • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
  • 缺点: 如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源. (而挂起等待的时候是 不消耗 CPU 的).

4.2 挂起等待锁

挂起等待锁是重量级锁的具体实现,

挂起等待锁是重量级锁,也是悲观锁

【挂起等待锁的优缺点】:

  • 缺点:一旦锁被释放,不能第一时间获取到
  • 优点:在锁被其他线程占用的时候,会放弃CPU资源

【举例理解自旋锁和挂起等待锁】

想象一下, 去追求一个女神. 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~
挂起等待锁: 陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意, 这个很长的时间间隔里, 女神可能已经换了好几个男票了).
自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手, 那么就能立刻抓住机会上位.

synchronized作为轻量级所的时候,内部是自旋锁作为重量级锁的时候,内部是挂起等待锁

五、公平锁和非公平锁

5.1 公平锁

假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待. 当线程 A 释放锁的时候, 会发生啥呢?

公平锁:遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁

5.2 非公平锁

非公平锁:不遵守 “先来后到”. B 和 C 都有可能获取到锁

这就好比一群男生追同一个女神. 当女神和前任分手之后, 先来追女神的男生上位, 这就是公平锁;
如果是女神不按先后顺序挑一个自己看的顺眼的, 就是非公平锁.

在这里插入图片描述

【注意】:

  • 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
  • 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.

synchronized是非公平锁

六、可重入锁和不可重入锁

6.1 可重入锁

public class Test2 {
    public static void fun(){
        //第一次对Test的类对象加锁
        synchronized (Test2.class){
            //第二次对Test的类对象加锁
            synchronized (Test2.class){

            }
        }
    }
    public static void main(String[] args) {
          fun();
    }
}

在这里插入图片描述

连续对同一个对象加锁两次,是一个很常见的问题,对于上述代码的写法还是很容易就能发现的,但下面的代码的这种写法就不容易被察觉到了

public class Test2 {
    
    public static synchronized void func1(){
        func2();
    }
    public static  synchronized void func2(){
        
    }
    public static void main(String[] args) {
          func1();
    }
}

为了上述的问题,就引入了"可重入锁"

可重入锁:即允许同一个线程多次获取同一把锁,在内部记录这个锁是哪个线程获取到的,如果发现当前要加锁的线程和持有锁的线程是同一个,则不挂起等待,而是直接获取到锁,同时还会在锁的内部加上计数器,记录当前是第几次加锁,每出一次解锁操作,计数器减一,当计数器为0时,才真正的释放锁,避免过早的释放锁

【补充】:

  • Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
  • Linux 系统提供的 mutex 是不可重入锁.

synchronized是可重入锁

6.2 不可重入锁

不可重入锁则可能发生上述的"死锁"

【关于锁的面试题】:

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
    悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁. 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据.
    在访问的同时识别当前的数据是否出现访问冲突. 悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据.
    获取不到锁就等待. 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突

  2. 介绍下读写锁? 读写锁就是把读操作和写操作分别进行加锁. 读锁和读锁之间不互斥. 写锁和写锁之间互斥. 写锁和读锁之间互斥. 读写锁最主要用在 “频繁读, 不频繁写” 的场景中.

  3. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
    如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁. 相比于挂起等待锁,
    优点:
    没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用. 缺点:
    如果锁的持有时间较长, 就会浪费CPU 资源

  4. synchronized是可重入锁吗?
    是可重入锁. 可重入锁指的就是连续两次加锁不会导致死锁. 实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340