多线程 --- 常见锁策略

常见的锁策略
乐观锁 vs 悲观锁
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

这两种思路不能说谁优谁劣, 而是看当前的场景是否合适

   Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.

读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁( readers-writer lock ),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
一个线程对于数据的访问 , 主要存在两种操作 : 读数据 和 写数据 .
 * 两个线程都只是读一个数据 , 此时并没有线程安全问题 . 直接并发的读取即可 .
 * 两个线程都要写一个数据 , 有线程安全问题 .
 * 一个线程读另外一个线程写 , 也有线程安全问题 .
读写锁就是把读操作和写操作区分对待 . Java 标准库提供了 ReentrantReadWriteLock , 实现了读写锁.
 * ReentrantReadWriteLock.ReadLock 类表示一个读锁 . 这个对象提供了 lock / unlock 方法进行
加锁解锁 .
 * ReentrantReadWriteLock.WriteLock 类表示一个写锁 . 这个对象也提供了 lock / unlock 方法进
行加锁解锁 .
其中 ,
 * 读加锁和读加锁之间, 不互斥 .
 * 写加锁和写加锁之间, 互斥 .
 * 读加锁和写加锁之间, 互斥 .
注意 , 只要是涉及到 " 互斥 ", 就会产生线程的挂起等待 . 一旦线程挂起 , 再次被唤醒就不知道隔了多
久了 .
因此尽可能减少 " 互斥 " 的机会 , 就是提高效率的重要途径
读写锁特别适合于 " 频繁读 , 不频繁写 " 的场景中 . ( 这样的场景其实也是非常广泛存在的 ).
Synchronized 不是读写锁
重量级锁 vs 轻量级锁
重量级锁 : 加锁机制重度依赖了 OS 提供了 mutex
* 大量的内核态用户态切换
* 很容易引发线程的调度
这两个操作 , 成本比较高 . 一旦涉及到用户态和内核态的切换 , 就意味着 " 沧海桑田 ".
轻量级锁 : 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成 . 实在搞不定了 , 再使用 mutex.
* 少量的内核态用户态切换 .
* 不太容易引发线程调度
synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁
自旋锁
按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU ,需要过很久才能再次被调度 .
但实际上 , 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.
伪代码:
while (抢锁(lock) == 失败) {}
如果获取锁失败 , 立即再尝试获取锁 , 无限循环 , 直到获取到锁为止 . 第一次获取锁失败 , 第二次的尝试会在极短的时间内到来.
一旦锁被其他线程释放 , 就能第一时间获取到锁 .
自旋锁是一种典型的 轻量级锁 的实现方式 .
* 优点 : 没有放弃 CPU, 不涉及线程阻塞和调度 , 一旦锁被释放 , 就能第一时间获取到锁 .
* 缺点 : 如果锁被其他线程持有的时间比较久 , 那么就会持续的消耗 CPU 资源 . ( 而挂起等待的时候是不消耗 CPU ).
synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的
公平锁 vs 非公平锁
假设三个线程 A, B, C。  A 先尝试获取锁 , 获取成功 . 然后 B 再尝试获取锁 , 获取失败 , 阻塞等待 ; 然后C 也尝试获取锁 , C 也获取失败 , 也阻塞等待 .
当线程 A 释放锁的时候 , 会发生啥呢 ?
公平锁: 遵守 " 先来后到 ". B C 先来的 . A 释放锁的之后 , B 就能先于 C 获取到锁 .
非公平锁: 不遵守 " 先来后到 ". B C 都有可能获取到锁 .
注意:
  * 操作系统内部的线程调度就可以视为是随机的 . 如果不做任何额外的限制 , 锁就是非公平锁 . 如果要想实现公平锁, 就需要依赖 额外的数据结构 , 来记录线程们的先后顺序 .
  * 公平锁和非公平锁没有好坏之分 , 关键还是看适用场景 .
synchronized 是非公平锁
可重入锁 vs 不可重入锁
可重入锁的字面意思是 可以重新进入的锁 ,即 允许同一个线程多次获取同一把锁
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是 可重入 (因为这个原因可重入锁也叫做 递归锁
Java 里只要以 Reentrant 开头命名的锁都是可重入锁,而且 JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
Linux 系统提供的 mutex 是不可重入锁 .
相关面试题
1) 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同一个共享变量冲突的概率较大 , 会在每次访问共享变量之前都去真正加
.
乐观锁认为多个线程访问同一个共享变量冲突的概率不大 . 并不会真的加锁 , 而是直接尝试访问数
. 在访问的同时识别当前的数据是否出现访问冲突 .
悲观锁的实现就是先加锁 ( 比如借助操作系统提供的 mutex), 获取到锁再操作数据 . 获取不到锁就
等待 .
乐观锁的实现可以引入一个版本号 . 借助版本号识别出当前的数据访问是否冲突 . ( 实现细节参考上
面的图 ).
2) 介绍下读写锁 ?
读写锁就是把读操作和写操作分别进行加锁 .
读锁和读锁之间不互斥 .
写锁和写锁之间互斥 .
写锁和读锁之间互斥 .
读写锁最主要用在 " 频繁读 , 不频繁写 " 的场景中 .
3) 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败 , 立即再尝试获取锁 , 无限循环 , 直到获取到锁为止 . 第一次获取锁失败 , 第二次的尝
试会在极短的时间内到来 . 一旦锁被其他线程释放 , 就能第一时间获取到锁 .
相比于挂起等待锁 ,
优点 : 没有放弃 CPU 资源 , 一旦锁被释放就能第一时间获取到锁 , 更高效 . 在锁持有时间比较短的场
景下非常有用 .
缺点 : 如果锁的持有时间较长 , 就会浪费 CPU 资源 .
4) synchronized 是可重入锁么?
是可重入锁 .
可重入锁指的就是连续两次加锁不会导致死锁 .
实现的方式是在锁中记录该锁持有的线程身份 , 以及一个计数器 ( 记录加锁次数 ). 如果发现当前加锁
的线程就是持有锁的线程 , 则直接计数自增 .
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值