admin管理员组文章数量:1794759
并发编程的奥秘:探索锁机制的多样性与应用
任何设计锁的场所,都设计锁策略,本篇文章主要揭秘实现一个锁需要知道的特性。
1.乐观锁和悲观锁
悲观锁 :
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略。
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 " 版本号 " 来解决。 假设我们需要多线程修改 "用户账户余额"。设当前余额为 100。引入一个版本号 version, 初始值为 1. 并且我们规定 "提交版本必须大于记录当前版本才能执行更新余额"。
2.读写锁
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁( readers-writer lock ),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
一个线程对于数据的访问 , 主要存在两种操作 : 读数据 和 写数据 .
- 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
- 两个线程都要写一个数据, 有线程安全问题.
- 一个线程读另外一个线程写, 也有线程安全问题.
读写锁就是把读操作和写操作区分对待 . Java 标准库提供了 ReentrantReadWriteLock 类 , 实现了读写
锁 .
- ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
- ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.
其中,
- 读加锁和读加锁之间, 不互斥.
- 写加锁和写加锁之间, 互斥.
- 读加锁和写加锁之间, 互斥.
读写锁特别适合于 "频繁读, 不频繁写" 的场景中. (这样的场景其实也是非常广泛存在的).
Synchronized 不是读写锁。
3.重量级锁和轻量级锁
锁的核心特性 " 原子性 ", 这样的机制追根溯源是 CPU 这样的硬件设备提供的 .
- CPU 提供了 "原子操作指令".
- 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
- JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
重量级锁 : 加锁机制重度依赖了 OS 提供了 mutex
- 大量的内核态用户态切换
- 很容易引发线程的调度
这两个操作的成本均相对较高。
轻量级锁 : 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成 . 实在搞不定了 , 再使用 mutex.
- 少量的内核态用户态切换.
- 不太容易引发线程调度
synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.
4.自旋锁
按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.
但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.
自旋锁伪代码:
代码语言:javascript代码运行次数:0运行复制while (抢锁(lock) == 失败) {}
如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.
一旦锁被其他线程释放 , 就能第一时间获取到锁 .
自旋锁是一种典型的 轻量级锁 的实现方式. 优点 : 没有放弃 CPU, 不涉及线程阻塞和调度 , 一旦锁被释放 , 就能第一时间获取到锁 . 缺点 : 如果锁被其他线程持有的时间比较久 , 那么就会持续的消耗 CPU 资源 . ( 而挂起等待的时候是不消耗 CPU 的 ).
synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的.
5.公平锁和非公平锁
公平锁 : 遵守 " 先来后到 ". B 比 C 先来的 . 当 A 释放锁的之后, B 就能先于 C 获取到锁。
非公平锁 : 不遵守 " 先来后到 ". B 和 C 都有可能获取到锁。
需要注意的是: 操作系统内部的线程调度就可以视为是随机的 . 如果不做任何额外的限制 , 锁就是非公平锁 . 如果要想实现公平锁, 就需要依赖 额外的数据结构 , 来记录线程们的先后顺序 . 公平锁和非公平锁没有好坏之分 , 关键还是看适用场景 .
synchronized 是非公平锁.
6.可重入锁和不可重入锁
可重入锁的字面意思是 “ 可以重新进入的锁 ” ,即 允许同一个线程多次获取同一把锁 。不会出现自己把自己锁死的情况。
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是 可重入 锁 (因为这个原因可重入锁也叫做 递归锁 ) 。
Java 里只要以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。而 Linux 系统提供的 mutex 是不可重入锁。
synchronized 是可重入锁
7.相关面试题
1.你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁. 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁 , 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突 . 悲观锁的实现就是先加锁( 比如借助操作系统提供的 mutex), 获取到锁再操作数据 . 获取不到锁就等待. 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突。
2.介绍下读写锁?
读写锁就是把读操作和写操作分别进行加锁 . 读锁和读锁之间不互斥 . 写锁和写锁之间互斥 . 写锁和读锁之间互斥 . 读写锁最主要用在 " 频繁读 , 不频繁写 " 的场景中 .
3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败 , 立即再尝试获取锁 , 无限循环 , 直到获取到锁为止 . 第一次获取锁失败 , 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放 , 就能第一时间获取到锁 . 相比于挂起等待锁: 优点 : 没有放弃 CPU 资源 , 一旦锁被释放就能第一时间获取到锁 , 更高效 . 在锁持有时间比较短的场景下非常有用. 缺点 : 如果锁的持有时间较长 , 就会浪费 CPU 资源 .
4.synchronized 是可重入锁么?
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2024-10-17,如有侵权请联系 cloudcommunity@tencent 删除数据线程并发并发编程操作系统是可重入锁 . 可重入锁指的就是连续两次加锁不会导致死锁 . 实现的方式是在锁中记录该锁持有的线程身份 , 以及一个计数器 ( 记录加锁次数 ). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增 .
本文标签: 并发编程的奥秘探索锁机制的多样性与应用
版权声明:本文标题:并发编程的奥秘:探索锁机制的多样性与应用 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1754731060a1705719.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论