文章目录
- 一、什么是死锁
- 二、死锁例子
-
- 例一:必然发生死锁
- 例二:银行转账
- 例三:多人银行转账
- 例四、哲学家就餐问题
- 三、死锁的必要条件
- 四、定位死锁
- 五、死锁的修复
-
- 银行转账修复
- 哲学家就餐问题修复
- 解决死锁的一种方式:设置超时时间
- 六、避免死锁的策略
- 七、活锁
-
- 例子:
- 解决办法:假如随机因子
- 八、总结
一、什么是死锁
当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。
死锁发生的几率不高,但是危害大,死锁一旦发生,会导致整个系统崩溃,子系统崩溃。
二、死锁例子
例一:必然发生死锁
public class MustDeadLock implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 0) {
synchronized (o1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("线程1拿到两把锁");
}
}
}
if (flag == 1) {
synchronized (o2) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("线程2拿到两把锁");
}
}
}
}
public static void main(String[] args) {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag = 0;
r2.flag = 1;
Thread thread1 = new Thread(r1);
Thread thread2 = new Thread(r2);
thread1.start();
thread2.start();
}
}
线程1和线程2都需要对法国持有的资源才能正常结束,所以有一直等待,发生死锁。
例二:银行转账
public class TransferMoney implements Runnable {
int flag = 1;
static Account a = new Account(500);
static Account b = new Account(500);
public static void transferMoney(Account from, Account to, int amount) {
synchronized (from) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功转账" + amount + "元");
}
}
}
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的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
@Override
public void run() {
if (flag == 1) {
transferMoney(a, b, 200);
}
if (flag == 0) {
transferMoney(b, a, 200);
}
}
static class Account {
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
这里的代码和上面的其实比较相似,都是因为在休眠之后拿不到所需的资源。
例三:多人银行转账
public class MultiTransferMoney {
private static final int NUM_ACCOUNTS = 500;
private static final int NUM_MONEY = 1000;
private static final int NUM_ITERATIONS = 1000000;
private static final int NUM_THREADS = 20;
public static void main(String[] args) {
Random rnd = new Random();
TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new TransferMoney.Account(NUM_MONEY);
}
class TransferThread extends Thread{
@Override
public void run() {
for (int i = 0; i < NUM_ITERATIONS; i++) {
int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
int toAcct = rnd.nextInt(NUM_ACCOUNTS);
int amount = rnd.nextInt(NUM_MONEY);
TransferMoney.transferMoney(accounts[fromAcct],accounts[toAcct],amount);
}
}
}
for (int i = 0; i < NUM_THREADS; i++) {
new TransferThread().start();
}
}
}
在经历了多次后,还是会死锁,原因就是转账时的资源占有,可能出现循环等待。
例四、哲学家就餐问题
public class DiningPhilosophers {
public static class Philosopher implements Runnable {
private Object leftChopstick;
private Object rightChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
try {
while (true) {
doAction("Thinking");
synchronized (leftChopstick) {
doAction("Picked up left chipstick");
synchronized (rightChopstick) {
doAction("Picked up right chipstick----eating");
doAction("Picked down right chipstick");
}
doAction("Picked down left chipstick");
}
}
} 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[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopsticks[i];
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
三、死锁的必要条件
- 互斥条件
- 请求与保持
- 不剥夺
- 循环等待
四、定位死锁
使用ThreadMXBean
public class ThreadMXBeanDetection implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 0) {
synchronized (o1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("线程1拿到两把锁");
}
}
}
if (flag == 1) {
synchronized (o2) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("线程2拿到两把锁");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
r1.flag = 0;
r2.flag = 1;
Thread thread1 = new Thread(r1);
Thread thread2 = new Thread(r2);
thread1.start();
thread2.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());
}
}
}
}
五、死锁的修复
银行转账修复
在银行转账发生的死锁中,主要的问题其实就是资源获取的顺序,如果我们能够让转账这件事有序的发生,而不是互相发生即可
static Object lock = new Object(); //当发生hash冲突时,就去竞争这把锁
public static void transferMoney(Account from, Account to, int amount) {
class Helper{
public void transfer(){
if (from.balance-amount<0){
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功转账" + amount + "元");
}
}
}
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash<toHash){
synchronized (from){ //号小的人先执行
synchronized (to){
new Helper().transfer();
}
}
}else if (fromHash>toHash){
synchronized (to){
synchronized (from){
new Helper().transfer();
}
}
}else{//若发生了冲突,就同时去竞争锁
synchronized (lock){
synchronized (to){
synchronized (from){
new Helper().transfer();
}
}
}
}
}
哲学家就餐问题修复
类似上面的解决办法,只要修改一个哲学家的就餐顺序即可:改变一个哲学家拿筷子的顺序(前面的哲学家都是先拿左手的筷子再拿右手的)
public static void main(String[] args) {
Philosopher[] philosophers = new Philosopher[5];
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopsticks[i];
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
//改变一个哲学家拿筷子的顺序就可以打破僵局
if (i == philosophers.length - 1) {
philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
} else {
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
}
// philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
解决死锁的一种方式:设置超时时间
利用tryLock,设置超时时间,如果拿不到需要的锁,就将现在的资源给释放掉。
public class TryLockDeadLock implements Runnable{
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag==1){
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)){
System.out.println("线程1获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
System.out.println("线程1获取到了两把锁");
lock2.unlock();
lock1.unlock();
break;
}else {
System.out.println("线程1尝试获取锁2失败,已重试");
//线程1将锁释放
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
}else{
System.out.println("线程1获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag==0){
try {
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)){
System.out.println("线程2获取到了锁2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(3000,TimeUnit.MILLISECONDS)){
System.out.println("线程2获取到了两把锁");
lock1.unlock();
lock2.unlock();
break;
}else {
System.out.println("线程2尝试获取锁1失败,已重试");
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
}else{
System.out.println("线程2获取锁2失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TryLockDeadLock r1 = new TryLockDeadLock();
TryLockDeadLock r2 = new TryLockDeadLock();
r1.flag=1;
r2.flag=0;
new Thread(r1).start();
new Thread(r2).start();
}
}
线程1没拿到锁就直接释放了,然后重新去尝试拿锁,这时候线程2已经执行完了,所以就可以顺利运行
六、避免死锁的策略
总结:
- 设置超时时间(tryLock)
- 使用系统中比较安全的并发类
- 减少每把锁控制的范围
- 避免锁嵌套
- 尽量将资源回收
- 养成良好的编码规范:给线程起个好名
七、活锁
对于上面这幅图,
-
死锁的解释是:
A:我是绅士,你起来我再起来。 B:我等你先起来我再起来
结果:一直等着吧。
-
活锁的解释是:
A说:你先起来吧。B说:你先起来吧。A说:不,你先起来吧。B说:不,你先起来吧。A说:不。 ……
结果:A和B一直在说话,然而谁也没起来。
活锁就是线程得到了资源,但是它却不能用,由于某些条件,它刚拿到的资源又要被让出去。就这样一直处于得到-释放的循环中。
例子:
牛郎和织女在吃饭,他们都只有在饿的时候才吃,并且,由于他们是夫妻,所以互相尊敬,在自己吃之前都要看对方饿不饿,如果对方饿,就给对方吃。
public class LiveLock {
static class Spoon {
private Diner owner;
public synchronized void use() {
System.out.printf("%s has eaten!", owner.name);
}
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return owner;
}
public void setOwner(Diner owner) {
this.owner = owner;
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) throws InterruptedException {
while (isHungry) {
if (spoon.owner != this) {
Thread.sleep(1);
continue;
}
if (spouse.isHungry) {
System.out.println(name + ":亲爱的" + spouse.name + "你先吃吧");
spoon.setOwner(spouse);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + ":我吃完了");
spoon.setOwner(spouse);
}
}
}
public static void main(String[] args) {
Diner husband = new Diner("牛狼");
Diner wife = new Diner("蜘女");
Spoon spoon = new Spoon(husband);
new Thread(new Runnable() {
@Override
public void run() {
try {
husband.eatWith(spoon, wife);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
wife.eatWith(spoon, husband);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
最后的结果:发生活锁
解决办法:假如随机因子
用一个随机函数,让牛郎有概率在饿的时候直接开吃不管之女。
Random random = new Random(); //解决活锁
if (spouse.isHungry && random.nextInt(10)<9){
// if (spouse.isHungry) {
System.out.println(name + ":亲爱的" + spouse.name + "你先吃吧");
spoon.setOwner(spouse);
continue;
}