🎈专栏链接:多线程相关知识详解
目录
1.乐观锁VS悲观锁
2.读写锁VS普通互斥锁
3.轻量级锁VS重量级锁
4.自旋锁VS挂起等待锁
5. 公平锁VS非公平锁
6.可重入锁VS不可重入锁
7.关于synchronized的锁策略以及自适应
1.乐观锁VS悲观锁
乐观锁:预测锁冲突的概率不高,因此做的工作就可以简单点
悲观锁:预测锁冲突的概率较高,因此做的工作就要复杂一些
锁冲突就是多个线程竞争同一把锁,会产生阻塞等待
举个例子:
乐观锁:A在学校的教室内卷的时候,就比较乐观,就觉得在教室学习的时候,不怕被B发现自己在偷偷内卷,而且B来教室内卷的概率比较少,就不关教室门,即使B进来发现A自己在内卷,也不影响A自己学习
悲观锁:B在教室内卷的时候,就比较悲观,觉得A天天想要学习,来教室内卷,怕A的成绩超过B自己,就每次都关教室门,即使A不来教室学习,也把门给关上,多做了一步锁门操作
这两种锁是站在加锁解锁的角度看待的,看的是加锁解锁的过程中所干的活是多还是少
2.读写锁VS普通互斥锁
读写锁:
Ⅰ:加读锁
Ⅱ:加写锁
读锁和读锁之间不会产生竞争
写锁和写锁之间会产生竞争
读锁和写锁会产生竞争
普通互斥锁:就如同Synchronized,当两个线程竞争同一把锁的时候就会产生阻塞等待
多个线程同时读一个变量并没有问题,而且读的场景相比于写的场景就多了很多,使用读写锁相比于普通互斥锁就减少了很多的锁竞争,大大的优化了效率
3.轻量级锁VS重量级锁
轻量级锁的加锁解锁开销比较少,典型的是纯用户态的加锁解锁逻辑,开销是比较少的
重量级锁的加锁解锁开销比较大,典型的是进入了系统内核态的加锁解锁逻辑,开销是比较大的
这两种锁是站在结果的角度看待最终加锁解锁消耗的时间是多还是少,和乐观锁与悲观锁并不一样
通常情况下乐观锁比较轻量,悲观锁比较重量,但是也并不绝对
4.自旋锁VS挂起等待锁
自旋锁:相当于是"忙等"的状态,大量消耗的CPU资源,反复询问当前锁是否就绪
挂起等待锁:先把CPU资源空闲出来去做其他的事情,过一段时间才询问当前锁是否就绪
举个例子:
在我们等人的时候,对方还没有到约定地点,一直反复的打电话催促就是自旋锁,而当你发现对方还没到的时候,就在约定的地点找个地方玩手机,叫他来了再在约定的地点旁边找我们一下,多消耗一点时间,却能够用这些时间去做其他的事情,时间被利用起来了,这就是挂起等待锁
自旋锁是轻量级锁的一种体现,挂起等待锁是重量级锁的一种体现
5. 公平锁VS非公平锁
公平锁:公平锁是先来后到,谁先来谁就拿到锁
非公平锁:多个线程同时竞争一把锁,有一个线程是比较晚来的,却比其他先来的线程先拿到锁
举个例子:
t1,t2,t3三个线程竞争同一把锁,t1先来的,所以t1先拿到了锁,这就叫公平锁.
而如果t3是晚来的,然后t3比其他两个线程先拿到了锁,这就叫非公平锁
操作系统默认的锁的调度,是非公平的情况
想要实现一个公平锁,就需要引入额外的数据结构,来记录线程加锁的顺序,需要一定的额外开销
6.可重入锁VS不可重入锁
可重入锁:同一个线程对同一把锁连续加锁两次不会造成死锁
不可重入锁:同一个线程对同一把锁连续加锁两次会造成死锁
举个例子:
有一次A去上教室学习把门给锁上了,后面要出去的时候突然想从窗户翻出去不走大门,过了一会A忘记了这件事情,等A回来教室的时候发现教室里面门锁着,就觉得里面有人,但实际上并没有人.然后A在这里敲门等里面的人过来开门,但却是永远也等不到里面有人来给A开门,A就会一直等下去,这就是死锁
针对这个代码,第一次能够加锁成功,而第二次加锁的时候就会加锁失败,因为锁已经被占用,就会在第二次加锁这里进行阻塞等待,等到第一把锁被解锁,第二次加锁才会成功.而第一把锁解锁成功的条件是要求执行完synchronized代码块,也就是要求第二把锁加锁成功
public class Demo3 {
static class Counter{
public synchronized void increase() {//加锁
increase2();
}
public void increase2(){
increase3();
}
public void increase3(){
increase4();
}
public synchronized void increase4(){//加锁
}
}
public static void main(String[] args) {
}
}
上面这个代码就很有可能会一不小心就写出来,因为调用了多个方法并不能直观的看出来,就会造成死锁
因此更好的做法是不要让上述情况死锁
针对上述情况,不会产生死锁的话,这样的锁就叫做可重入锁,反而就叫不可重入锁
synchronized是可重入锁
可重入锁的实现要点:
Ⅰ.让锁里持有线程对象,记录是谁加了锁
Ⅱ.维护一个计数器,用来衡量啥时候是真加锁,啥时候是真解锁,啥时候是直接放行
引入一个计数器,每次加锁的时候计数器就++,每次解锁的时候计数器就--,如果计数器为0,此时的加锁才是真加锁,同样计数器为0,此时的解锁才是真解锁
如果程序抛出了异常,没有人catch就脱离了之前的代码块,脱离了一层代码,计数器就-1,脱离到计数器为0,也就解锁了,同理加锁代码中出现异常,也是不会死锁的,因为Java使用关键字结合代码块来做解锁操作,无论如何解锁代码都能执行到的
7.关于synchronized的锁策略以及自适应
①既是乐观锁,也是悲观锁
②既是轻量级锁,也是重量级锁
③既是自旋锁,也是挂起等待锁
④不是读写锁,是普通互斥锁
⑤是非公平锁
⑥是可重入锁
synchronized是自适应的,初始使用的时候,是 乐观锁/轻量级锁/自旋锁,如果竞争不激烈则保持这个状态不变,如果锁竞争激烈了,synchronized会自动升级成为 悲观锁/重量级锁/挂起等待锁,所以synchronized是"智能"的