主要参考How2J系列教程
一、死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
用大白话讲就是:
- 线程A 首先占有对象A,接着试图占有对象B
- 线程B 首先占有对象B,接着试图占有对象A
- 线程A 等待线程B 释放对象B
- 与此同时,线程B等待 线程A 释放对象A
两个线程将会一直相互等待,这就叫做死锁
死锁的代码演示:
public static void main(String[] args) {
final String drink = "饮料";
final String food = "食物";
Thread t1 = new Thread(){
public void run(){
//已经有了吃的
synchronized (drink) {
System.out.println("t1 已占有饮料");
try {
//停顿1000毫秒,另一个线程有足够的时间占有食物
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 试图占有食物");
System.out.println("t1 等待中 。。。。");
synchronized (food) {
System.out.println("do something");
}
}
}
};
Thread t2 = new Thread(){
public void run(){
//占有食物
synchronized (food) {
System.out.println("t2 已占有食物");
try {
//停顿1000毫秒,另一个线程有足够的时间占饮料
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 试图占有饮料");
System.out.println("t2 等待中 。。。。");
synchronized (drink) {
System.out.println("do something");
}
}
}
};
t1.start();
t2.start();
}
二、线程的交互
既然有死锁的问题,那么就需要一个方法来避开这种情况,于是就有了wait、notify两个方法。
- this.wait()表示 让占有this的线程等待,并临时释放占有。
- this.notify() 表示通知那些等待在this的线程,可以苏醒过来了。
我们新建一个hero类
public class Hero {
public String name;
public Integer hp;
public synchronized void recover() {
hp++;
System.out.printf("%s 回血1点,增加血后,血量是%d %n", name, hp);
this.notify();
}
public synchronized void getHurt() {
if (hp <= 1) {
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
hp--;
System.out.printf("%s 减血1点,血量是%d %n", name, hp);
}
}
主函数如下所示:
public static void main(String[] args) {
Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 60;
Thread hurtThread = new Thread() {
@Override
public void run() {
while (true) {
gareen.getHurt();
try {
// 攻击间隔10ms
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
hurtThread.start();
Thread recoverThread = new Thread() {
@Override
public void run() {
while (true) {
gareen.recover();
try {
// 治疗间隔
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
recoverThread.start();
}
盖伦 减血1点,血量是10
盖伦 减血1点,血量是9
盖伦 减血1点,血量是8
盖伦 减血1点,血量是7
盖伦 减血1点,血量是6
盖伦 减血1点,血量是5
盖伦 减血1点,血量是4
盖伦 减血1点,血量是3
盖伦 减血1点,血量是2
盖伦 回血1点,增加血后,血量是3
盖伦 减血1点,血量是2
盖伦 减血1点,血量是1
盖伦 回血1点,增加血后,血量是2
盖伦 减血1点,血量是1
盖伦 回血1点,增加血后,血量是2
盖伦 减血1点,血量是1
盖伦 回血1点,增加血后,血量是2
盖伦 减血1点,血量是1
留意wait()和notify() 这两个方法是什么对象上的?
public synchronized void hurt() {
。。。
this.wait();
。。。
}
public synchronized void recover() {
。。。
this.notify();
}
这里需要强调的是,wait方法和notify方法,并不是Thread线程上的方法,它们是Object上的方法。
因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。
-
wait()
的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。 -
notify()
的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。 -
notifyAll()
的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。
因为我们仅有一个线程会陷入沉睡,所以此处的notifyAll()
的效果,与notify()
相同,若我们有多个线程则会有效果。
此时我们对Hero进行小改造,每次回复5HP,并且标出是谁(哪个线程)打的盖伦
public synchronized void recover() {
hp += 5;
System.out.printf("%s 回血5点,增加血后,血量是%d %n", name, hp);
this.notify();
}
public synchronized void getHurt(String who) {
if (hp <= 1) {
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
hp--;
System.out.printf("%s出手 ———————————————— %s 减血1点,血量是%d %n",who, name, hp);
}
结果如下
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是59
盖伦 回血1点,增加血后,血量是64
EZ出手 ———————————————— 盖伦 减血1点,血量是63
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是62
EZ出手 ———————————————— 盖伦 减血1点,血量是61
.....
盖伦 回血5点,增加血后,血量是6
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是5
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是4
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是3
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是2
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是1
盖伦 回血5点,增加血后,血量是6
EZ出手 ———————————————— 盖伦 减血1点,血量是5
EZ出手 ———————————————— 盖伦 减血1点,血量是4
EZ出手 ———————————————— 盖伦 减血1点,血量是3
EZ出手 ———————————————— 盖伦 减血1点,血量是2
EZ出手 ———————————————— 盖伦 减血1点,血量是1
可以看见之前是在触发wait()之前,是两个英雄在攻击盖伦(两个线程同时在运行),触发wait()后,两个线程都停止了,每次recover() 内执行的notify()只唤醒一个线程。
若我们把notify(),改为notifyAll(),Hero改动代码如下所示:
public synchronized void recover() {
hp += 5;
System.out.printf("%s 回血5点,增加血后,血量是%d %n", name, hp);
this.notifyAll();
}
结果如下所示
盖伦 回血5点,增加血后,血量是6
EZ出手 ———————————————— 盖伦 减血1点,血量是5
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是4
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是3
EZ出手 ———————————————— 盖伦 减血1点,血量是2
EZ出手 ———————————————— 盖伦 减血1点,血量是1
盖伦 回血5点,增加血后,血量是6
EZ出手 ———————————————— 盖伦 减血1点,血量是5
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是4
吴亦凡出手 ———————————————— 盖伦 减血1点,血量是3
EZ出手 ———————————————— 盖伦 减血1点,血量是2
EZ出手 ———————————————— 盖伦 减血1点,血量是1
可发现,每次唤醒的都是两个线程。