别翻了,常见的锁策略就在这里了~

Posted 你这家伙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了别翻了,常见的锁策略就在这里了~相关的知识,希望对你有一定的参考价值。

咱们其他也就不多说了,就直奔主题了~

1.乐观锁 VS 悲观锁

悲观锁:总是假设最坏的情况,每次取拿数据的时候都认为别人会修改,所以在拿数据的时候都会上锁,这样别热你想拿到这个数据就会阻塞直到它拿到锁。(多个线程同时竞争锁)

乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式堆数据是否并发冲突进行检测,如果并发冲突了,则会返回用户错误信息,让用户决定如何取做。

悲观锁存在的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程,认为多个线程同时竞争锁的概率很高,但是愿意花更多的成本来解决锁竞争问题(也正是因为总是在竞争锁没所以他更安全,但是代价更大
乐观锁存在的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度,认为多个线程同时竞争锁的概率较低,会花更少的成本来解决锁竞争问题(所以它并没有那么安全,但是代价小)

比如两个人流落荒野,但是身上只剩下了两个馒头,此时乐观的人说“还好我们身上有两个馒头”;但是悲观的人说“我们只有两个馒头了”

注意:其实两种锁并没有谁好谁不好,只是在不同的场景下,选用不同的策略

2.读写锁

所谓的读写锁在加锁的时候分为两种情况

  • 加读锁:线程只能读取数据,不能修改数据
  • 加写锁:线程可以读取数据,也可以修改数据
    (有些人有疑惑,为什么加写锁既能读也能写呢?其实可以理解成,都没有读取到数据,又怎么去修改呢?哈哈哈~ 开个玩笑~)

其实在实际的开发过程中,很多场景都是“一写多读”,这里的“一写多读”不是只只写一次,而是写的次数非常少,而读的次数非常多(比如:在博客上面看到的文章很多,但是评论的很少)

使用读写锁,效率就会比较高

  • 读锁与读锁之间,不会产生互斥(既不会发生锁竞争问题),因为发生死锁的线程不安全情况是多个线程去“修改 ”同一个变量的时候,才会不安全,而读锁只能读不能修改,那么也就不会出现锁竞争问题了~
  • 读锁与写锁之间,写锁与写锁之间才会发生互斥(既发生锁竞争)


因为大部分都是在进行读操作,写的操作是很少的,只有触发写操作的时候才会真的进行锁竞争,于是这个读写锁的效率就会很高

3.重量级锁 VS 轻量级锁

所谓的 重量 / 轻量 指的是 锁竞争的开销大还是不大

锁之所以能够保持原子性,是因为应用层序依赖JVM,JVM又依赖操作系统,操作系统依赖于硬件设备。
也就是应用程序如果想要加锁,保证原子性的机制是JVM提供的,而JVM里面的原子性又是操作系统提供的,操作系统的原子性是硬件设备提供的,硬件设备的原子性就是物理上的东西了~

重量级锁:实现的过程中就大量的依赖了操作系统提供的功能(mutex),所以成本就比较高(大量的操作都是在内核态完成的)
轻量级锁:实现的过程中,只有迫不得已的地方才依赖操作系统的功能,所以成本就比较低(在内核态完成的工作较少)

举例:


也就是有的工作内核也能做,用户态也能做
重量级锁:就把这些工作都交给内核了
轻量级锁:就是自己尽量把自己能做的都做了,实在做不了了在交给内核去做

4.自旋锁 VS 挂起等待锁

他是一种轻量级锁(也就是很多工作在用户态就完成了),和自旋锁相对的是挂起等待锁

当多个线程去竞争一把锁的时候,如果发现锁已经被别人占用了的时候的两种情况:

  • 挂起等待锁,线程放弃CPU上的执行权限,该线程的PCB就会被内核放到对应的等待队列中,直到锁被释放之后,才有机会被唤醒
  • 自旋锁,线程先别放弃CPU,而是快速反复的去查询当前锁的占用状态

自选锁的优点是什么?
自旋锁的效率会高于挂起等待锁,因为CPU是稀缺资源,线程能在CPU上执行是来之不易的事情,挂机等待锁发现锁被占用就立刻放弃CPU就会很可惜,而自旋锁发现锁被占用的时候,不会立刻释放,而是会快速反复的查询锁的状态,当几个周期之后锁被释放了的话,此时这个线程就会快速的获取到锁资源

举例
比如现在去买包子,但是你去的时候发现已经卖完了,但是你有两种选择,一种是扭头回家,过一会再来看看(挂起等待锁);另一种就是在周围转转,时不时的就过来看看有没有做好的包子(自旋锁)。

自旋锁带来的代价就是要付出更多的CPU资源(比如在周围等包子做好的期间,你在周围转转的时间就是被浪费了)

5.公平锁 VS 非公平锁

当有若干个线程都想竞争锁,来了一个线程A,线程A来了的时候正好锁被释放了这时候:
公平锁:线程A排到原来的线程后面,等待前面的线程都把锁使用完成了之后,线程A再获取锁
非公平锁:线程A直接把锁抢走

比如:当小王去买包子的时候,发下此时有很多人在排队,那么如果是正常排在所有排队人的后面等到自己的时候再去买包子,这就是“公平锁”,如果直接插队到第一个去买包子,这就是“不公锁”

公平锁和非公平锁哪个更好呢?
其实这都是根据情况而定的,不能直接说谁好谁不好
对于公平锁而言,能够让每个线程“雨露均沾”,但是要想实现公平锁,那么至少需要维护一个队列来排队,所以要付出更多的代价
对于非公平锁而言,虽然每个线程获取锁的概率不俊航,但是实现更简单
(自旋锁更容易触发“非公平锁”)

6.可重入锁

如果一个线程,针对同一把锁,连续加锁两次,不会造成死锁,就是可重入锁;如果造成了死锁,就是不可重入锁

锁如果不加特殊处理,就是不可重入的,如果记录锁是谁加的,此时就可以实现可重入锁(也就是判断现在想要获取锁的线程和正在持有锁的线程是否是同一个线程,如果是就放行,这也就实现了可重入锁)

产生死锁的三种典型场景:

  1. 一个线程,一把锁(也就是上面提到的连续加锁两次,且为不可重入锁的情况)
  2. 两个线程1,2,两把锁A,B,线程1获取锁A(成功),线程2获取锁B(成功),接下来,线程1尝试获取锁B,线程2同时尝试获取锁B,此时就会发生死锁
  3. N个线程N把锁,此时更容易出现死锁(哲学家就餐问题)

7.一个真实锁的出现,往往会同时具有多种策略(synchronized举例)

  1. 一开始是乐观锁,后面可能会转换成悲观锁。(如最开始锁竞争不是那么激烈的时候就是乐观锁,当中间锁竞争激烈的时候就会自动转换成悲观锁)
  2. 开始是轻量级锁,后面可能会膨胀成轻量级锁(刚开始我们采用轻量级锁来实现,但是当过了很久都获取不到锁的资源的时候,就会变成重量级锁),此处的轻量级锁基于自旋锁实现的,重量级锁基于挂机等待锁(内核完成挂起等待)
  3. 不公平锁
  4. 可重入锁
  5. 不是读写锁

8.常见的面试题

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
  2. 有了解什么读写锁么?
  3. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
  4. synchronized 是可重入锁么?

以上是关于别翻了,常见的锁策略就在这里了~的主要内容,如果未能解决你的问题,请参考以下文章

别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析JVM篇二

多线程常见的锁策略

多线程常见的锁策略

多线程常见的锁策略

别翻了,Lambda 表达式入门,看这篇就够了

别翻了,成员变量和局部变量在多线程中的使用,看这篇就够了