目录
写在前面
这一部分内容,对实际开发其实用处不大,但是在校招中是一个极其高频的考点,也就是我们所说的“八股文”内容,这部分内容,能理解掌握最好,不能理解掌握,背下来也可以,但是我的初心是用最简洁直白的文字描述清每一个知识点,争取让大家都掌握到这些知识点。所以我会尽可能的详细介绍这些策略,力求让大家不需要死记硬背也可以牢牢掌握。
一、乐观锁 VS 悲观锁
锁的实现者,通过预测接下来锁冲突(锁竞争,前文介绍过的抢厕所例子)的概率是大还是不大,根据这个冲突的概率,来决定接下来该咋做。
乐观锁和悲观锁最终要做的事情是不一样的
以刚经历过的疫情为例:网络上根据即将要放开的政策大致分为了两个派系:
- 乐观派:认为放开也没什么事,生活会恢复正常,除了基本的戴口罩以外不做任何防护策略。
- 悲观派:认为放开会对自己生活产生很大影响,生活不会恢复正常,除了戴口罩以外还买了消毒酒精、粮食、药物,还自主隔离等等措施
这里两个派系应对同一事情做出的不同反应就类似于乐观锁和悲观锁
- 乐观锁:所做的工作会少一些,效率更高。
- 悲观锁:所做的工作会多一些,效率更低。
二、轻量级锁 VS 重量级锁
字面上来解读:
轻量级锁更为轻量,重量级锁更为重量,这里的轻量重量体现在加锁解锁上,它和上面的乐观悲观锁有一定重合但不是一回事
- 一个乐观锁很可能也是一个轻量级锁
- 一个悲观锁很可能是一个重量级锁
这并不绝对,只是可能性较大
轻量级锁是用户态操作,重量级锁会交给内核阻塞等待
三、自旋锁 VS 挂起等待锁
自旋锁是轻量级锁的一种典型实现,挂起等待锁是重量级锁的一种典型实现。
举一个生动形象的例子:沸羊羊
即便美羊羊已经明确表示沸羊羊只是个好人,但是沸羊羊仍然锲而不舍的每天关心问候美羊羊,这就是一个典型的自旋锁,加锁失败后,这个锁不会去做别的事情,而是锲而不舍的在此空转,直到美羊羊失恋,沸羊羊就可以第一时间乘虚而入,拿下美羊羊。
那么沸羊羊应该怎么样做才能体面一点呢,那就是从自旋锁变成挂起等待锁,当美羊羊表示对它没意思的时候,它可以先去做别的事情提高自己,等待美羊羊被喜羊羊伤透了心时,回过头来发现沸羊羊已经变的非常优秀,它自然就会接受沸羊羊了,这里沸羊羊被拒绝后不在围着美羊羊转,而是去做自己的事情,就是一个典型的挂起等待锁
自旋锁:一旦锁被释放,自旋锁可以第一时间拿到锁,速度会更快,但缺点是这个锁会一直消耗cpu空转等待(忙等)。通常是纯用户态操作,不需要经过内核态
挂起等待锁:如果锁被释放,不能第一时间拿到锁。通过内核的机制来挂起等待,时间更长。
四、互斥锁 VS 读写锁
互斥锁:加锁就是加锁,没有额外更细化的区分
sychronized就是一个读写锁,进入代码块就加锁,出了代码块就解锁
读写锁:有更细化的区分
- 1.给读加锁
- 2.给写加锁
- 3.解锁
读写锁保证了多个线程读同一个变量不会涉及线程安全问题
读写锁还约定了
- 1.读锁和读锁直接不会产生锁竞争,自然也不会有阻塞等待:不会影响程序速度
- 2.写锁和写锁直接有锁竞争,会阻塞等待:会影响速度,但保证准确性
- 3.读锁和写锁之间有锁竞争,会阻塞等待:会影响速度,但保证准确性
核心思想:非必要不加锁
使用场景:一写多读的情况
标准库提供了两个专门的读写锁,即读锁是一个类,写锁是一个类,将两个锁独立区分开,可以更为方便灵活的使用
五、可重入锁 VS 不可重入锁
- 可重入锁:一个锁在一个线程中对该锁加锁两次仍然不死锁,叫可重入锁
- 不可重入锁:一个锁在一个线程中对该锁加锁两次,死锁了,叫不可重入锁
举一个比较玄幻的例子
这个例子中,小滑稽去上厕所,但是时空扭曲它被传送了出来,此时门被锁上了,为了上厕所它就需要等待锁释放,而锁是它自己锁上的,锁释放就需要它自己去释放,但是它此时无法释放,这样就造成了死锁,这也就是一个不可重入锁
也就是形如这样的代码
Object locker = new Object();
synchronized (locker){
synchronized (locker){
}
}
还有一种情况
synchronized void put(int elem){
this.size();....
}
synchronized int size(){
.....;
}
}
这里就不会导致死锁,synchronized是一个“可重入锁”,在这个场景中,synchronized在加锁时会判定一下当前线程是否已经是锁的拥有者了,如果是,直接放行。
六、公平锁 VS 非公平锁
一个锁,遵循先来后到的顺序上锁就是公平锁,不遵循就是不公平锁
公平锁:
非公平锁:
synchronized在默认情况下是非公平锁,想实现公平锁需要在synchronized的基础上加队列来记录加锁的顺序
死锁
死锁的情况:
一个线程,一把锁,可重入锁没事,不可重入锁死锁。
两个线程,两把锁,可重入锁也会死锁
例如:我把车钥匙落在了家里,把家钥匙落在了车里,为了拿到车钥匙我要先拿到家钥匙,但是为了拿到家钥匙我又必须要拿到车钥匙,逻辑冲突,死锁。
哲学家就餐问题
五个哲学家在餐桌上吃饭,他们一共有两种行为
- 拿起筷子吃饭
- 放下筷子思考人生
这几个哲学家都非常的固执,要吃饭,必须要拿到两根筷子才能吃,并且如果它们只拿到了一根筷子,就会一直等待自己拿到第二根筷子吃到饭以后才会放下筷子,否则就会一直僵持住。
假设现在是这种情况
所有人都拿起了自己右手的筷子,所有人都拿到了一根筷子,但是由于需要两根筷子才能吃饭,而且它们又非常的固执,这就会导致,它们要吃饭,就要等待别人放下筷子,但是别人又不会放下筷子,这样大家都僵持住了,也就导致了死锁的发生。
死锁的四个必要条件
- 互斥使用:一共线程拿到一把锁以后另一个线程不能使用
- 不可抢占:一共线程拿到锁以后除非自己主动释放别的线程不能强行占有
- 请求和保持:一个线程拿到一把锁以后会去争取拿到另一把锁(代码特点)
- 循环等待:逻辑依赖循环(代码特点)
解决死锁问题的思路
约定加锁顺序:如果在上面的哲学家问题 给每根筷子编号,并且约定,只要所有人都先拿编号小的筷子,在拿编号大的筷子,就不会死锁。
只要所有线程都遵循这个顺序,就可以解决问题了
sychronized是什么样的锁?
- sychronized既是乐观锁,也是悲观锁
开始时是乐观锁,当锁冲突频繁时,转换为悲观锁
- sychronized既是轻量级锁,也是重量级锁
开始时是轻量级锁,如果锁被持有的时间较长,转为重量级锁
- 轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现
- 不是读写锁
- 是可重入锁
- 是非公平锁
sychronized会自动根据当前锁的竞争程度,自适应的调整锁策略
- 竞争不激烈:以轻量级/乐观锁状态运行
- 竞争激烈:以重量级/悲观锁的状态运行
sychronized的关键策略
锁升级
偏向锁:偏向锁是一种加锁机制,一个锁首先会对一个线程进行标记,如果有其他线程尝试对这个线程加锁,首先会修改这个标记,如果在一段时间内,没有任何线程尝试对这个线程加锁,这个标记不会被修改,锁也不会真正被添加,但是在这段时间内如果有锁尝试对这个线程加锁,会修改这个标记,此时偏向锁会立刻上锁,抢占这个线程。
核心思维:非必要,不加锁
锁消除
核心思维:非必要,不加锁
sychronized并非一个高效的开发手段,在实际开发中,过多的锁会拖累程序运行效率,为了避免这种情况,sychronized引入了锁消除的策略,即在编译阶段检测当前代码是否多线程执行,是否有必要加锁,如果没有加锁的必要,就会在编译过程中自动把锁去掉
锁粗化
先来介绍一个概念
锁的粒度:sychronized代码块,包含代码的多少,代码越多,锁的粒度越大,越少,粒度越轻
一般写代码时,多数情况下是希望锁的粒度更小一些(串行执行的代码少,并发执行的代码多)。
但是如果某段代码中,涉及到频繁的加锁、解锁操作,编译器则会可能把这段代码优化成一个更粗粒度的锁,这就是sychronized的锁粗化策略
本章到这里就结束了,下一章介绍CAS-一个与多线程密切相关的东西