三种实现多线程方式的对比分析
多线程的实现方式有三种,其中Runnable接口和Callable接口实现多线程的方式基本相同,主要区别就是Callable接口中的方法有返回值,并且可以声明抛出异常。那么通过继承Thread类和其他两种接口实现多线程的方式有什么区别呢?接下来通过一个应用场景来分析说明。
假设售票厅有四个窗口可发售某日某次列车的100张车票,这100张车票就可以看做共享资源,四个售票窗口相当于四个线程,为了更直观显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前线程的实例对象,然后调用getName()方法获取到线程名称。接下来,通过继承Thread类的方式来实现多线程,如文件1所示。
文件1 Example04.java
1 // 1、定义一个继承Thread线程类的子类
2 class TicketWindow extends Thread {
3 private int tickets = 100;
4 public void run() {
5 while (true) {
6 if (tickets > 0) {
7 System.out.println(Thread.currentThread().getName()
8 + " 正在发售第 " + tickets-- + " 张票 ");
9 }
10 }
11 }
12 }
13 public class Example04 {
14 public static void main(String[] args) {
15 // 分别创建4个线程对象TicketWindow并开启,来模拟4个售票窗口
16 new TicketWindow().start();
17 new TicketWindow().start();
18 new TicketWindow().start();
19 new TicketWindow().start();
20 }
21 }
运行结果如图1所示。
图1 运行结果
从图1可以看出,每张票都被发售了四次。出现这种现象的原因是:四个线程没有共享100张票,而是各自出售了100张票。在程序中创建了四个TicketWindow对象,就等于创建了四个售票程序,而每个TicketWindow对象中都有一个tickets票据变量,这样每个线程在执行任务时都会独立地处理各自的资源,而不是共同处理同一个售票资源。
小提示:
文件1中创建并启动4个线程时并没有指定线程的名称,但运行结果却显示有Thread-0、Thread-1...这样的线程名称。这是因为,在创建多线程时如果没有通过构造方法指定线程名称,则系统默认生成线程名称。
由于现实中,售票系统中的票资源是共享的,因此上面的运行结果显然不合理。为了保证售票资源共享,在程序中只能创建一个售票对象,然后开启多个线程去共享同一个售票对象来执行售票方法,简单来说就是四个线程运行同一个售票程序,这时就需要通过实现Runnable接口的方式来实现多线程。
接下来将文件1进行修改,并使用构造方法Thread(Runnable target, String name)在创建线程对象的同时指定线程的名称,如文件2所示。
文件2 Example05.java
1 // 定义一个实现Runnable接口的实现类
2 class TicketWindow2 implements Runnable {
3 private int tickets = 100;
4 public void run() {
5 while (true) {
6 if (tickets > 0) {
7 System.out.println(Thread.currentThread().getName()
8 + " 正在发售第 " + tickets-- + " 张票 ");
9 }
10 }
11 }
12 }
13 public class Example05 {
14 public static void main(String[] args) {
15 // 创建TicketWindow实例对象tw
16 TicketWindow2 tw = new TicketWindow2();
17 // 分别创建4个线程对象同时进行命名,并开启线程
18 new Thread(tw, "窗口1").start();
19 new Thread(tw, "窗口2").start();
20 new Thread(tw, "窗口3").start();
21 new Thread(tw, "窗口4").start();
22 }
23 }
运行结果如图2所示。
图2 运行结果
文件2中,通过实现Runnable接口的方式只创建了一个TicketWindow2对象,然后创建了四个线程,在每个线程上都调用同一个TicketWindow2对象中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票。
通过上面的售票案例,并结合实际情况进行分析,通过实现Runnable接口(或者Callable接口)相对于继承Thread类实现多线程来说,有如下显著的好处:
(1)适合多个线程去处理同一个共享资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想。
(2)可以避免Java单继承带来的局限性,由于一个类不能同时有两个父类,所以在当前类已经有一个父类的基础上,那么就只能采用实现Runnable接口或者Callable接口的方式来实现多线程。
事实上,实际开发中大部分的多线程应用都会采用Runnable接口或者Callable接口的方式实现多线程。