开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第29天,点击查看活动详情
问题
线程之间有交互通知的需求,考虑如下情况:
有两个线程,处理同一个英雄,一个加血,一个减血。
减血的线程,发现血量 = 1,就停止减血,知道加血的线程为英雄加了血,加可以继续减血。
一、不好的解决方式
1)、故意设计减血线程频率更高,盖伦的血量迟早会到达 1
2)、减血线程中使用 while 循环判断 hp 是否为 1,如果是 1 就不停的循环,知道加血线程回复了血量
3)、这种方式会大量占用 cpu ,拖慢性能
代码实现:
//1、新建一个 Hero 类
public class Hero{
public String name;
public float hp;
public int damage;
public synchronized void recover(){
hp=hp+1;
}
public synchronized void hurt(){
hp=hp-1;
}
public void attackHero(Hero h) {
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}
public boolean isDead() {
return 0>=hp?true:false;
}
}
//2、测试
public class TestThread {
public static void main(String[] args) {
final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
Thread t1 = new Thread(){
public void run(){
while(true){
//因为减血更快,所以盖伦的血量迟早会到达1
//使用while循环判断是否是1,如果是1就不停的循环
//直到加血线程回复了血量
while(gareen.hp==1){
continue;
}
gareen.hurt();
System.out.printf("t1 为%s 减血1点,减少血后,%s的血量是%.0f%n",gareen.name,gareen.name,gareen.hp);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
while(true){
gareen.recover();
System.out.printf("t2 为%s 回血1点,增加血后,%s的血量是%.0f%n",gareen.name,gareen.name,gareen.hp);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t2.start();
}
}
//打印结果
t1 为盖伦 减血1点,减少血后,盖伦的血量是615
t2 为盖伦 回血1点,增加血后,盖伦的血量是616
t1 为盖伦 减血1点,减少血后,盖伦的血量是615
t1 为盖伦 减血1点,减少血后,盖伦的血量是614
...
二、使用 wait 和 notify 进行线程交互
1)、在 Hero 类中:hurt() 减血方法:当 hp = 1 的时候,执行 this.wait(),this.wait() 表示让占有 this 的线程等待,并临时释放占有。
2)、进入 hurt() 方法的线程必然是减血线程,this.wait() 会让减血线程临时释放堆 this 的占有,这样加血线程,就有机会进行 recover() 加血方法了。
3)、recover() 加血方法:增加了血量,执行 this.notify()。
4)、 this.notify() 表示通知那些等待在 this 的线程,可以苏醒过来了。等待在 this 的线程,恰恰是减血线程。
5)、一旦 recover() 结束,加血线程释放了 this,减血线程,就可以重新占有 this,并执行后面的减血工作。
代码实现:
//1、Hero 类改造
class Hero {
public String name;
public float hp;
public int damage;
public synchronized void recover() {
hp = hp + 1;
System.out.printf("%s 回血1点,增加血后,%s的血量是%.0f%n", name, name, hp);
// 通知那些等待在this对象上的线程,可以醒过来了,如第20行,等待着的减血线程,苏醒过来
this.notify();
}
public synchronized void hurt() {
if (hp == 1) {
try {
// 让占有this的减血线程,暂时释放对this的占有,并等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
hp = hp - 1;
System.out.printf("%s 减血1点,减少血后,%s的血量是%.0f%n", name, name, hp);
}
public void attackHero(Hero h) {
h.hp -= damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n", name, h.name, h.name, h.hp);
if (h.isDead())
System.out.println(h.name + "死了!");
}
public boolean isDead() {
return 0 >= hp ? true : false;
}
}
//2、测试
public class TestThread {
public static void main(String[] args) {
final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
Thread t1 = new Thread(){
public void run(){
while(true){
gareen.hurt();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
while(true){
gareen.recover();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t2.start();
}
}
//打印结果
盖伦 减血1点,减少血后,盖伦的血量是615
盖伦 回血1点,增加血后,盖伦的血量是616
盖伦 减血1点,减少血后,盖伦的血量是615
盖伦 减血1点,减少血后,盖伦的血量是614
...
盖伦 回血1点,增加血后,盖伦的血量是2
盖伦 减血1点,减少血后,盖伦的血量是1
盖伦 回血1点,增加血后,盖伦的血量是2
盖伦 减血1点,减少血后,盖伦的血量是1
...
三、关于 wait,notify,notifyAll
这里需要强调的是,wait 方法和 notify 方法,并不是Thread线程上的方法,它们是 Object 上的方法。
因为所有的 Object 都可以被用来作为同步对象,所以准确的讲,wait 和 notify 是同步对象上的方法。
wait() 的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。
notify() 的意思是:通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。
notifyAll() 的意思是:通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。
四、总结
本篇文章我们通过一个多线程交互的问题,引出了对 wait ,notify 的介绍和使用,注意 wait ,notify 等方法都是 Object 上的方法,因为所有的 Object 都可以当作同步对象,所以更准确的讲,wait 和 notify 是同步对象上的方法。
好了,本篇文章到这里就结束了,感谢你的阅读🤝