目录
1、死锁
2、活锁
3、饥饿
一个资源应该单独使用一把锁。
比如,一个对象中有多个共享资源,但有多个线程需要使用其中的不同资源
此时如果把对象整体作为一把锁,那并发就很低。
可以考虑,把每个共享资源都单独拆出来,分别上锁,这样每个线程都能各取所需,提高了并发度。
坏处是,可能导致线程死锁。
线程的代码是有限的,但由于某种原因,线程一直执行不完,称为线程的活跃性。
活跃性有三种原因:死锁、活锁、饥饿。
1、死锁的定义
死锁:一组互相竞争资源的线程因互相等待获取对方的资源,导致线程一直阻塞的情况。
2、为什么会产生死锁
一个线程需要同时获取多把锁时,就容易引发死锁。
简单的例子:
它俩又不会释放自己已经持有的锁,一直耗着,产生了死锁。
总结
如果锁应用不当,造成“两个线程都在等待对方释放锁
”,那么它们会一直等待,无法运行,这就发生了死锁。
简单说,死锁问题是由两个或以上的线程并行执行时,因为争抢资源而造成互相等待造成的
。
3、死锁的四要素
只要同时满足这四个条件,就肯定会死锁:
4、避免死锁的三种思路
避免死锁,只需要破坏掉四大条件的其中一个即可。
互斥条件没办法破坏,本来锁的目的就是互斥
所以只需要破坏以下三项中的其中一项即可:
一次性申请所有的锁
,不能单独持有某一个锁获取不到锁时,就先主动释放持有的所有锁
获取锁的顺序
注意:三种思路,但是在具体的场景中,每种做法的开销都是不同的,需要找到开销最低的方式。
一般来说:
5、避免死锁的三种实现方式
1、破坏持有并等待条件
一次性申请所有锁这个动作属于临界区,应该抽取出一个类来管理,让它作为锁,向外提供两个同步方法:同时获取所有锁、同时释放所有锁。
由于它要作为锁,所以这个对象必须是单例的。
在尝试申请所有资源时,使用while()死循环,注意要加上超时判断。
class Allocator { // 维护一个资源列表 private List
此处while()与synchronized锁粗粒度的区别
在申请“锁”时,两种方式都是串行的。
但是,while()在通过单例对象获取到全部资源后,只需要申请所需的资源,不会影响其他无关的同类操作,可以并行执行。
而synchronized锁粗粒度,在执行时也只能串行执行。
2、破坏不可抢占条件
线程在获取不到锁时,释放手头持有的锁。这一点Java在语言层面是做不到的,不过Java在SDK层面实现了。
因为 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了。线程进入阻塞状态后啥都干不了,也释放不了线程已经占有的资源。
java.util.concurrent 包下的 Lock 是可以轻松解决这个问题的。
它提供了一个方法:tryLock(long, TimeUnit),在一段时间内尝试获取锁,如果最终没有获取到,就执行释放锁的逻辑。
3、破坏循环等待条件
破坏这个条件,需要对资源进行排序,然后按序申请资源,这样就能避免多个线程交叉加锁的情况。
比如转账,就在代码中写死,按照账户id从小到大依次加锁,就不会有问题。
1、什么是活锁
活锁是指,线程没有发生阻塞,但依然执行不下去的情况。
2、活锁的例子
如果两个线程互相改变对方的结束条件,就可能导致双方谁也无法结束。
比如这个程序:
public class TestLiveLock { static volatile int count = 10; static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { // 期望减到 0 退出循环 while (count > 0) { sleep(0.2); count--; log.debug("count: {}", count); } }, "t1").start(); new Thread(() -> { // 期望超过 20 退出循环 while (count < 20) { sleep(0.2); count++; log.debug("count: {}", count); } }, "t2").start(); } }
相当于一个抽水一个注水,水池永远不会空或者满
3、死锁与活锁的区别
4、活锁的解决方案
增加随机的睡眠时间,将这样的两个线程错开执行,只要一方先运行完了,那么另一方也就能运行完
1、什么是线程饥饿
饥饿指的是线程因无法访问所需资源而无法执行下去的情况:
2、线程饥饿的解决方案
有三种方案: