本文已参与「新人创作礼」活动,一起开启掘金创作之路。
需求:
现有20个苹果,分别有阿猫、阿狗、阿鸭去取这堆苹果并将他们吃掉(每一次一个人只能吃一个)。使用多线程方式演示该过程。
public class AppleThread extends Thread {
private static int appleCount = 20;
public AppleThread(String name) {
super(name);
}
@Override
public void run() {
while (appleCount > 0) {
System.out.println(this.getName() + "吃掉了第 " + appleCount-- + " 个苹果");
}
}
}
public class TestDemo {
public static void main(String[] args) {
AppleThread t1 = new AppleThread("阿猫");
AppleThread t2 = new AppleThread("阿狗");
AppleThread t3 = new AppleThread("阿鸭");
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
}
}
阿狗吃掉了第 19 个苹果
阿猫吃掉了第 20 个苹果
阿鸭吃掉了第 18 个苹果
阿猫吃掉了第 16 个苹果
阿狗吃掉了第 17 个苹果
阿猫吃掉了第 14 个苹果
阿鸭吃掉了第 15 个苹果
阿猫吃掉了第 12 个苹果
阿狗吃掉了第 13 个苹果
......
很容易发现,打印的结果存在问题,为什么他们打印的顺序不是有序的?
虽然苹果的个数appleCount是静态的,他们共享的是同一堆苹果,但他们取苹果的速度不一样,吃苹果的速度也不一样,就以上面的打印结果为例,最先抢到苹果的是阿猫,但最先吃完苹果的是阿狗……这就导致了上面的结果。
那我们要是想让他们有序,即最先拿到苹果的人要吃完别人才能去取,这该怎么做?
这个时候我们就可以使用同步锁解决这个问题。
使用同步锁有两种方式:
- 第一种是同步代码块
- 第二种是同步方法
我们就使用同步方法的方式吧,直接看代码!
public class AppleThread extends Thread {
private static int appleCount = 20;
public AppleThread(String name) {
super(name);
}
// 这里添加了synchronized关键字
@Override
synchronized public void run() {
while (appleCount > 0) {
System.out.println(this.getName() + "吃掉了第 " + appleCount-- + " 个苹果");
}
}
}
阿鸭吃掉了第 19 个苹果
阿猫吃掉了第 20 个苹果
阿狗吃掉了第 18 个苹果
阿猫吃掉了第 16 个苹果
阿鸭吃掉了第 17 个苹果
阿猫吃掉了第 14 个苹果
阿狗吃掉了第 15 个苹果
阿猫吃掉了第 12 个苹果
阿鸭吃掉了第 13 个苹果
阿猫吃掉了第 10 个苹果
......
咦,奇怪了!不是加了synchronized嘛?为什么还是一样的结果?是因为加在了run方法上吗?
不不不,这个时候我们就要想到一个知识点了!
使用synchronized修饰方法时,同步锁有两种情况:
-
若修饰的是静态方法,那同步锁就是当前类的字节码(即类名.class)
-
若修饰的不是静态方法,那同步锁就是当前对象(即this)
很明显,run方法并不是静态方法!而我们在ThreadDemo类中创建了三个对象,然后同步锁又是this,就相同于每个对象都有一把钥匙去开这个锁,所以这个锁存在与不存在都是一样的。
那有的人就会想,那不简单嘛,直接在run()方法上添加static,这不就行了??? 这就行了???你可以试试!
添加完static后,我们会发现@Override这个注解会有一个红色波浪线,并告诉你Method does not override method from its superclass(方法不会覆盖其超类中的方法),也就是说加了static后就不会重写父类中的方法,所以不能这样做滴!
那怎么办呢?
这个时候我们可以将里面的代码抽取出来,自定义一个吃苹果的方法,看代码!
public class AppleThread extends Thread {
private static int appleCount = 20;
public AppleThread(String name) {
super(name);
}
@Override
public void run() {
// 调用方法
eatApple();
}
// 吃苹果方法
synchronized static public void eatApple() {
while (appleCount > 0) {
System.out.println(this.getName() + "吃掉了第 " + appleCount-- + " 个苹果");
}
}
}
将代码抽取出来之后,你会发现eatApple()方法里面的this.getName()报错了!
what?why?
因为该方法是静态的,静态的方法是不能引用对象的任何成员的,所以这里this.getName()就应当修改为Thread.currentThread().getName(),再次运行代码,看结果!
Thread-0吃掉了第 20 个苹果
Thread-0吃掉了第 19 个苹果
Thread-0吃掉了第 18 个苹果
Thread-0吃掉了第 17 个苹果
Thread-0吃掉了第 16 个苹果
Thread-0吃掉了第 15 个苹果
Thread-0吃掉了第 14 个苹果
Thread-0吃掉了第 13 个苹果
Thread-0吃掉了第 12 个苹果
......
呀!好像确实成功了诶,达到了我们的目的,吃苹果的顺序有序了!
莫名其妙
但是,你会发现没有?苹果全被Thread-0吃了,被他全吃了也就算了。我们居然还不知道是被谁吃的!!!
这是怎么回事?我们先把问题罗列一下,逐个解决。
-
问题一:苹果为什么全被一个人吃了?
-
问题二:为什么吃苹果的人没有名字?
我们先来思考第一个问题:
仔细研究下eatApple()这个方法,我们就会发现(while循环),当有一个人进来后,他就不出去了,除非苹果全部被吃完了。等他吃完之后再出去,还在锁外面等候的两个人就会发现,已经没有苹果了!所以只好遗憾地离开......
那如何解决这个问题呢?
既然是while出了问题,那我们就不能在锁里面使用循环了,所以我们这里应当改为if,判断是否还有苹果。
若有再进去吃(避免出现负数),然后为了保证20个苹果都能够被吃完,我们还需要在run()方法中使用一个for循环(while也可以,这里就不演示了)。
这里我添加了一个sleep()方法,目的是为了让效果能够明显一些(不加的话可能需要多试几次才能看到效果)。
写好就飞起,看效果!
public class AppleThread extends Thread {
private static int appleCount = 20;
public AppleThread(String name) {
super(name);
}
@Override
public void run() {
// 添加for循环
for (int i = 0; i < 20; i++) {
// 模拟网络延迟
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
eatApple();
}
}
synchronized static public void eatApple() {
// 将while修改为if
if (appleCount > 0) {
System.out.println(Thread.currentThread().getName() + "吃掉了第 " + appleCount-- + " 个苹果");
}
}
}
Thread-2吃掉了第 20 个苹果
Thread-1吃掉了第 19 个苹果
Thread-0吃掉了第 18 个苹果
Thread-2吃掉了第 17 个苹果
Thread-1吃掉了第 16 个苹果
Thread-0吃掉了第 15 个苹果
Thread-1吃掉了第 14 个苹果
Thread-0吃掉了第 13 个苹果
Thread-2吃掉了第 12 个苹果
Thread-2吃掉了第 11 个苹果
Thread-0吃掉了第 10 个苹果
......
很明显,我们目的达到了,苹果不再被一个人独吞了,而是被三个人共享。
只不过我们现在还不知道他们是谁,所以下面来解决第二问题!
Thread-x这样的名字很明显就是虚拟机给线程起的名字,那我们不是给线程起了名字?为什么没有覆盖掉呢?
Are you sure? 我们仔细再次把代码看下就会发现,我们是给自定义子线程起了名字,但并没有给线程取名字!
所以问题出在TestDemo中!虽然我们创建u1、u2、u3对象时赋予了他们名字,但这是自定义子线程的名字,并不是我们后面创建的线程的名字,所以运行程序的时候jvm就get不到他们的名字啦!
so,我们现在不给子线程起名字了,直接给线程起名字(API文档可以查到存在该构造器,或者查看源码也可以看到),如下代码↓,改好飞起!
注意:这里需要在AppleThread类中添加一个无参构造器
public class TestDemo {
public static void main(String[] args) {
AppleThread u1 = new AppleThread();
AppleThread u2 = new AppleThread();
AppleThread u3 = new AppleThread();
new Thread(u1, "阿猫").start();
new Thread(u2, "阿狗").start();
new Thread(u3, "阿鸭").start();
}
}
阿鸭吃掉了第 20 个苹果
阿狗吃掉了第 19 个苹果
阿猫吃掉了第 18 个苹果
阿狗吃掉了第 17 个苹果
阿猫吃掉了第 16 个苹果
阿鸭吃掉了第 15 个苹果
阿猫吃掉了第 14 个苹果
阿鸭吃掉了第 13 个苹果
阿狗吃掉了第 12 个苹果
......
虽然我们的要求达到了,但严格意义上说,并不需要创建线程去调用start()方法,只需要用我们创建的子线程对象去调用就可以了,请看代码!
说明:上面之所以那样写是因为有些初学者会将继承Thread和实现Runnable接口的方式混淆!
public class TestDemo {
public static void main(String[] args) {
AppleThread u1 = new AppleThread("阿猫");
AppleThread u2 = new AppleThread("阿狗");
AppleThread u3 = new AppleThread("阿鸭");
// 修改为如下↓
u1.start();
u2.start();
u3.start();
}
}
浅入分析
这里简单分析下,Thread的构造器明明要的是Runnable类型的参数,为什么可以将子线程对象作为参数传给它?
相应的源码如下:
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
通过查看源码或API文档,我们可以发现AppleThread、Thread、Runnable之间有这样的关系:
AppleThread extends Thread、Thread implements Runnable
所以AppleThread是Runnable的实现类的子类,我们可以简单认为AppleThread就是Runnable的子类,那么子类对象作为参数传给父类对象,其实就隐含了向上转型操作,因此,上面那样写就没出毛病啦!
另辟蹊径
对于上面解决名字问题的方法,其实还有一种解决方案,就是从run()方法中获取到调用run方法的对象的名字,然后传参到eatApple()方法,代码如下:
public class AppleThread extends Thread {
private static int appleCount = 20;
public AppleThread() {}
public AppleThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取当前对象的名称
eatApple(this.getName());
}
}
// 有参的eatApple方法
synchronized static public void eatApple(String name) {
if (appleCount > 0) {
System.out.println(name + "吃掉了第 " + appleCount-- + " 个苹果");
}
}
}