什么是死锁?所谓死锁,是指多个线程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,多个线程都无法继续执行,总而导致死锁现象。
下面演示一种必定发生死锁的情况
package DeadLock;
/**
* @program:多线程和IO
* @descripton:打开注释,就会死锁。
* @author:ZhengCheng
* @create:2021/9/30-21:08
**/
public class DeadLockDemo{
static Object lock1 =new Object();
static Object lock2 =new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new a());
Thread t2 = new Thread(new b());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("end");
}
static class a implements Runnable{
@Override
public void run() {
synchronized (lock1){
System.out.println("getLock1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
/*try {
lock1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}*/
System.out.println("getLock1+Lock2");
}
}
}
}
static class b implements Runnable{
@Override
public void run() {
synchronized (lock2){
System.out.println("getLock2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1){
/*try {
lock2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}*/
System.out.println("getLock2+Lock1");
}
}
}
}
}
上面演示的就是一种死锁的情况。死锁虽然发生的概率小,但是一旦死锁,就会出现很大的问题。我们用两个转账的案例,来模拟现实中可能出现的死锁情况。
第一种是两个人互相转账
package DeadLock;
/**
* @program:多线程和IO
* @descripton:转账时遇到死锁,一旦打开注释
* @author:ZhengCheng
* @create:2021/9/30-21:41
**/
public class TransferMoney implements Runnable{
int flag = 1;
static Account a = new Account(500);
static Account b= new Account(500);
public static void main(String[] args) throws InterruptedException {
TransferMoney r1 = new TransferMoney();
TransferMoney r2 = new TransferMoney();
r1.flag = 1;
r2.flag = 0;
Thread thread1 = new Thread(r1);
Thread thread2 = new Thread(r2);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(a.balance);
System.out.println(b.balance);
System.out.println("end");
}
@Override
public void run() {
if (flag == 1){
transferMoney(a,b,200);
}
if (flag == 0){
transferMoney(b,a,300);
}
}
public static void transferMoney(Account from , Account to ,int money ){
synchronized (from){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to){
if (from.balance - money < 0){
System.out.println("余额不足");
return;
}else {
from.balance -= money;
to.balance += money;
System.out.println("交易成功,余额增加"+money);
}
}
}
}
public static class Account{
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
第二种是很多个人随机转账。(开启多个线程)
package DeadLock;
import DeadLock.TransferMoney.Account;
import java.util.Random;
/**
* @program:多线程和IO
* @descripton:几千个人,开启多个线程,看是否会发生死锁,更具有普遍性
* @author:ZhengCheng
* @create:2021/10/1-22:21
**/
public class TransferManyMan {
private static int Num_Acct = 500;
private static int Num_Money = 1000;
private static int Num_Trans = 1000000;
private static int Num_Threads = 20;
public static void main(String[] args) {
Account[] accounts = new Account[Num_Acct];
for (int i = 0; i < Num_Acct; i++) {
accounts[i] = new Account(Num_Money);
}
Random random = new Random();
for (int i = 0 ; i< Num_Threads ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < Num_Trans; i++) {
Account from_acc = accounts[random.nextInt(Num_Acct)];
Account to_acc = accounts[random.nextInt(Num_Acct)];
TransferMoney.transferMoney(from_acc,to_acc,random.nextInt(Num_Money));
}
}
}).start();
}
}
}
最后不能发现,执行上述代码,都发生了死锁现象。可见,死锁的产生,对我们日常使用以及是有极大的危害的。
从上面是三个案例,我们可以总结一下发生死锁的几个必要条件
♦ 互斥条件 ♦ 请求与保持条件 ♦ 不剥夺条件 ♦ 循环等待条件
当死锁发生以后,我们必然会想定位死锁发生的位置。
如何定位呢?
使用jstack命令行
使用ThreadMXBean(主要)
下面使用之前的Demo,我们使用ThreadMXBean来找到发生死锁的线程。
package DeadLock;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
/**
* @program:多线程和IO
* @descripton:打开注释,就会死锁。
* @author:ZhengCheng
* @create:2021/9/30-21:08
**/
public class ThreadMXBeanDetection implements Runnable {
public ThreadMXBeanDetection(int flag) {
this.flag = flag;
}
int flag;
static Object lock1 = new Object();
static Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException {
ThreadMXBeanDetection td1 = new ThreadMXBeanDetection(1);
ThreadMXBeanDetection td2 = new ThreadMXBeanDetection(0);
Thread t1 = new Thread(td1);
Thread t2 = new Thread(td2);
t1.start();
t2.start();
Thread.sleep(1000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0){
for (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("发现了死锁:"+threadInfo.getThreadName());
}
}
}
@Override
public void run() {
if (flag == 1) {
synchronized (lock1) {
System.out.println("getLock1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("getLock1+Lock2");
}
}
}
if (flag == 0) {
synchronized (lock2) {
System.out.println("getLock2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("getLock2+Lock1");
}
}
}
}
}
能够找到死锁的位置,那么我们肯定要尝试去修复死锁的问题。
修复死锁的问题有三种解决方法。
1.避免死锁(通过对代码的设计,避免死锁的发生)
2.检测与恢复策略:一段时间检测是否有死锁,如果有就波多某一个资源,来打开死锁。
3.鸵鸟策略
那么下面以最经典的哲学家就餐问题,来展示修复死锁的各种方法。
首先写出哲学家就餐的demo
package DeadLock;
/**
* @program:多线程和IO
* @descripton:哲学家就餐问题
* @author:ZhengCheng
* @create:2021/10/2-21:33
**/
public class DiningTable {
public static class Philosopher implements Runnable{
private Object leftChop ;
private Object rightChop;
public Philosopher(Object leftChop, Object rightChop) {
this.leftChop = leftChop;
this.rightChop = rightChop;
}
@Override
public void run() {
while (true){
try {
doAction("Thinking");
synchronized (leftChop){
doAction("Get left Chop");
synchronized (rightChop){
doAction("Get right chop");
doAction("EAT");
doAction("Put down right chop");
}
doAction("Put down left chop");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName()+" "+action);
Thread.sleep((long)Math.random()*10);
}
}
public static void main(String[] args) {
Philosopher[] philosophers = new Philosopher[5];
Object[] chops = new Object[philosophers.length];
for (int i = 0; i < chops.length; i++) {
chops[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object left = chops[i];
Object right = chops[(i+1)%philosophers.length];
philosophers[i] = new Philosopher(left ,right);
new Thread(philosophers[i],"哲学家"+(i+1)+"号").start();
}
}
}
解决哲学家就餐问题,我们有许多种方法。按照方法同样归属于最初的三种。
避免原则:通过逻辑设计,让其规避死锁的风险。只要锁构成的有向图没有成环,那么就没有死锁的风险。于是给出一个经典的解决方案,让其中一个哲学家,换手操作。
检测与恢复策略:我们可以通过对其进行管理,在发生死锁时,强制让某人或者全部停下,释放锁。
鸵鸟原则:不推荐
换手操作只需要在最后创建哲学家对象时进行修改即可
for (int i = 0; i < philosophers.length; i++) {
//解决哲学家问题,换手策略,让一个哲学家,换一个方向拿东西
Object left = chops[i];
Object right = chops[(i+1)%philosophers.length];
if (i == philosophers.length-1){
philosophers[i] = new Philosopher(right,left);
}else {
philosophers[i] = new Philosopher(left ,right);
}
new Thread(philosophers[i],"哲学家"+(i+1)+"号").start();
}
那么我们在实际的开发中,如何有效的避免死锁呢?
1.设置超时时间。使用try-lock的方式。
2.多使用并发类,而不自己创建类(ConcurrentHashMap之类的类,原子类等)
3.尽量降低锁的使用粒度,尽可能使用不同的锁而不是同一个锁。
4.尽量使用同步代码块,不适用同步方法。同时使用同步代码块,方便我们控制,自己指定锁
5.新建线程时,要为线程起有意义的名字,方便debug和排查。
6.避免出现锁的嵌套
7.分配资源前能不能收回来:银行家算法
8.尽量不要几个功能使用同一把锁。
活锁(Livelock):
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。但是活锁始终会处于运行状态,甚至比死锁更糟糕。
下面用一个经典的就餐问题,演示活锁的情况。当两人就餐时,只有一个勺子,两个人都十分的谦让,看到对方没有勺子,就会想让给对方。这样就会发生活锁。
package DeadLock;
import java.util.Random;
/**
* @program:多线程和IO
* @descripton:牛郎织女演示活锁
* @author:ZhengCheng
* @create:2021/10/3-15:59
**/
public class LiveLockDemo {
static class Spoon {
private Owner owner;
public Spoon(Owner owner) {
this.owner = owner;
}
public Owner getOwner() {
return owner;
}
public void setOwner(Owner owner) {
this.owner = owner;
}
public synchronized void use() {
System.out.printf("%s吃完了!", owner.name);
}
}
static class Owner {
private String name;
private boolean isHungry;
public Owner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Owner lover) {
while (isHungry) {
if (spoon.owner != this) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
if (lover.isHungry /*&& new Random().nextInt(10) < 9*/) {
System.out.println(name + ":" + lover.name + "你先吃");
spoon.setOwner(lover);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + "吃完了");
spoon.setOwner(lover);
}
}
}
public static void main(String[] args) {
Owner o1 = new Owner("a");
Owner o2 = new Owner("b");
Spoon spoon = new Spoon(o1);
new Thread(new Runnable() {
@Override
public void run() {
o1.eatWith(spoon,o2);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
o2.eatWith(spoon,o1);
}
}).start();
}
}
上述代码就会发生活锁的情况。两个线程互相谦让,导致没有人开始执行任务。想要解决这个问题,我们最好使用几种方法。比如设计一个谦让的规则,让其不活锁,或者我们可以增加一些随机性,而让其不是百分百谦让。上述的解决方法,只需将代码的注释激活即可。
饥饿:
饥饿 ,与死锁和活锁非常相似。是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。有可能是因为线程的优先级设置得过低,或者有某线程始终持有锁同时有无限循环从而不释放锁,或者某程序始终占用某文件的写锁。
常见面试问题
1.写一个必然死锁的例子。
2.发生死锁必须满足哪些条件(4个) 互斥、请求与保持、不剥夺、循环等待
3.如何定位死锁
4.解决死锁的策略(3个)
5.讲一讲哲学家就餐问题
6.实际工程中如何避免死锁?(8)
7.什么是活跃性问题?活锁、饥饿和死锁有什么区别?
1658

被折叠的 条评论
为什么被折叠?



