任何设计锁的场所,都设计锁策略,本篇文章主要揭秘实现一个锁需要知道的特性。
悲观锁 :
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略。
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 " 版本号 " 来解决。 假设我们需要多线程修改 "用户账户余额"。设当前余额为 100。引入一个版本号 version, 初始值为 1. 并且我们规定 "提交版本必须大于记录当前版本才能执行更新余额"。
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁( readers-writer lock ),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
一个线程对于数据的访问 , 主要存在两种操作 : 读数据 和 写数据 .
读写锁就是把读操作和写操作区分对待 . Java 标准库提供了 ReentrantReadWriteLock 类 , 实现了读写
锁 .
其中,
读写锁特别适合于 "频繁读, 不频繁写" 的场景中. (这样的场景其实也是非常广泛存在的).
Synchronized 不是读写锁。
锁的核心特性 " 原子性 ", 这样的机制追根溯源是 CPU 这样的硬件设备提供的 .
重量级锁 : 加锁机制重度依赖了 OS 提供了 mutex
这两个操作的成本均相对较高。
轻量级锁 : 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成 . 实在搞不定了 , 再使用 mutex.
synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.
按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.
但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.
自旋锁伪代码:
while (抢锁(lock) == 失败) {}
如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.
一旦锁被其他线程释放 , 就能第一时间获取到锁 .
自旋锁是一种典型的 轻量级锁 的实现方式. 优点 : 没有放弃 CPU, 不涉及线程阻塞和调度 , 一旦锁被释放 , 就能第一时间获取到锁 . 缺点 : 如果锁被其他线程持有的时间比较久 , 那么就会持续的消耗 CPU 资源 . ( 而挂起等待的时候是不消耗 CPU 的 ).
synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的.
公平锁 : 遵守 " 先来后到 ". B 比 C 先来的 . 当 A 释放锁的之后, B 就能先于 C 获取到锁。
非公平锁 : 不遵守 " 先来后到 ". B 和 C 都有可能获取到锁。
需要注意的是: 操作系统内部的线程调度就可以视为是随机的 . 如果不做任何额外的限制 , 锁就是非公平锁 . 如果要想实现公平锁, 就需要依赖 额外的数据结构 , 来记录线程们的先后顺序 . 公平锁和非公平锁没有好坏之分 , 关键还是看适用场景 .
synchronized 是非公平锁.
可重入锁的字面意思是 “ 可以重新进入的锁 ” ,即 允许同一个线程多次获取同一把锁 。不会出现自己把自己锁死的情况。
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是 可重入 锁 (因为这个原因可重入锁也叫做 递归锁 ) 。
Java 里只要以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。而 Linux 系统提供的 mutex 是不可重入锁。
synchronized 是可重入锁
1.你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁. 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁 , 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突 . 悲观锁的实现就是先加锁( 比如借助操作系统提供的 mutex), 获取到锁再操作数据 . 获取不到锁就等待. 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突。
2.介绍下读写锁?
读写锁就是把读操作和写操作分别进行加锁 . 读锁和读锁之间不互斥 . 写锁和写锁之间互斥 . 写锁和读锁之间互斥 . 读写锁最主要用在 " 频繁读 , 不频繁写 " 的场景中 .
3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败 , 立即再尝试获取锁 , 无限循环 , 直到获取到锁为止 . 第一次获取锁失败 , 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放 , 就能第一时间获取到锁 . 相比于挂起等待锁: 优点 : 没有放弃 CPU 资源 , 一旦锁被释放就能第一时间获取到锁 , 更高效 . 在锁持有时间比较短的场景下非常有用. 缺点 : 如果锁的持有时间较长 , 就会浪费 CPU 资源 .
4.synchronized 是可重入锁么?
是可重入锁 . 可重入锁指的就是连续两次加锁不会导致死锁 . 实现的方式是在锁中记录该锁持有的线程身份 , 以及一个计数器 ( 记录加锁次数 ). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增 .
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有