一. 常见的锁策略
1.1 乐观锁和悲观锁
说到乐观和悲观这两个概念,大家都不陌生,生活中我们也要常常面对一些乐观和悲观的时候,但是这是站在自身的角度去看待的,有的人看待一件事他认为是乐观的,而有的人认为他是悲观的;这里的 "乐观" 和 "悲观" 和我们说的乐观锁和悲观锁也是很相似的;
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据;
举个例子:假设有A 和 B 两个线程,他们要去获取数据,因为他们是乐观的,所以双方不会认为他们去修改数据,所以他们就拿到数据后执行各自的事情去了,还有一个特点就是线程A和B在更新共享数据之前,他们要去判断这个共享数据是否被其他线程修改,如果没有修改的话,那么就直接更新内存中共享变量的值,那如果被修改了,就会报错或者去执行其他相关的操作了
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁;
举个例子:还是有A 和 B两个线程,A 和 B要去拿数据,因为它是悲观的,所以在拿数据时需要进行加锁,假设A拿到了锁,那么B就会进入阻塞等待的状态,知道A释放锁,CPU会唤醒等待的线程B,B才能拿到这个锁,从而对数据进行操作;
总体来说,悲观锁一般要做的工作多一点,效率会更低一些;而乐观锁要做的事少一点,效率更高一点;
1.2 轻量级锁和重量级锁
轻量级锁:加锁和解锁的过程中更快更高效;
重量级锁:加锁和解锁的过程中更慢更低效;
这里看来,轻量级,重量级虽然和乐观,悲观不是一回事,但是有那么一定的相似,可以认为一个乐观锁可能是一个轻量级锁,但不是绝对的;关于这块后面还是会细说;
1.3 自旋锁和挂起等待锁
自旋锁是轻量级锁的一种代表实现,当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock);
优点:自旋锁一旦被释放,就能第一时间拿到锁,自旋锁是纯用户态操作,所以速度很快;
缺点:要一直等待,会消耗CPU资源
挂起等待锁是重量级锁的一直代表实现,当某个线程没有申请到锁的时候,此时该线程会被挂起,即加入到等待队列等待。当锁被释放的时候,就会被唤醒,重新竞争锁;
优点:不需要盲等,在等待的过程中可以参与别的事情,充分利用了CPU的资源;
缺点:如果锁被释放,不能第一时间拿到锁,挂起等待锁是通过内核的机制来实现,所以时间会更长,效率会更低;
1.4 互斥锁和读写锁
互斥锁:互斥锁是一个非常霸道的存在,比如有线程A,B,当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞,
我们学过的Synchronized就是互斥锁;
对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。如下图:
注:互斥锁和自旋锁最大的区别:
互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
读写锁:它由 读锁 和 写锁 两部分构成,如果只读取共享资源用 读锁 加锁,如果要修改共享资源则用 写锁 加锁。
读写锁一般有 3 种情况:
1.给读加锁
2.给写加锁
3.解锁
读写锁中的约定:
- 读锁和读锁之间,不会有锁竞争,不会产生阻塞等待
- 写锁和写锁之间,有锁竞争
- 读锁和写锁之间,有锁竞争
1.5 可重入锁和不可重入锁
针对一个线程,针对一把锁,连续加锁两次,如果出现死锁了,那就是不可重入锁,如果不死锁,那就是可重入锁;
Object locker = new Object(); synchronized(locker){ synchronized(locker){ } }
像上述这样的代码就是加锁两次的情况,第二次加锁需要等待第一个锁释放,第一个锁释放,需要等待第二个锁加锁成功,所以这种情况就矛盾了,但是并不会真正的死锁,因为synchronized是可重入锁,加锁的时候会先判定一下,看当前尝试申请锁的线程是不是已经拥有锁了,如果是的话,就不会矛盾;
synchronized 和 ReentrantLock 都是可重入锁,可重入锁最大的意思就是为了防止死锁;
1.6 公平锁和非公平锁
首先从字面意思理解,先到的线程会优先获取资源,后到的会进行排队等待,这种是公平的,而非公平锁是不遵循这个原则的,其实也很好理解,看下图:
这种情况就是公平的,遵循先到先得的规矩;
而像这种情况,就是非公平的,存在 "插队" 的现象;
1.7 关于锁策略的相关面试题
1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁;
乐观锁认为多个线程访问同一个共享变量冲突的概率不大,并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突;
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待;
2. 介绍下读写锁?
读写锁就是把读操作和写操作分别进行加锁;
读锁和读锁之间不互斥;
写锁和写锁之间互斥;
写锁和读锁之间互斥;
读写锁最主要用在 "频繁读,不频繁写" 的场景中;
3. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源
4. synchronized 是可重入锁么?
是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁
5. synchronized的特点:
- 既是乐观锁,也是悲观锁
- 既是轻量级锁,也是重量级锁
- 轻量级锁是基于自选锁实现的,重量级锁是基于挂起等待锁实现的
- 不是读写锁
- 是可重入锁
- 是非公平锁