1.
以下是重点,面试常问!
- Java中,类仅支持单继承,如果一个类继承了Thread类,就无法再继承其它类,因此,如果一个类既要继承其它的类,又必须创建为一个线程,就可以使用实现Runable接口的方式。
- 使用实现Runable接口的方式创建的线程可以处理同一资源,实现资源的共享。
- 使用实现Callable接口的方式创建的线程,可以获取到线程执行的返回值、是否执行完成等信息。
关于第2点,可以通过如下示例来理解。
假如我们总共有10张票(共享的资源),为了提升售票的效率,开了3个线程来售卖,代码如下所示:
package com.zwwhnly.springbootaction.javabase.thread;
public class SaleTicketThread implements Runnable { private int quantity = 10; @Override public void run() { while (quantity > 0) { System.out.println(quantity-- + " is saled by " + Thread.currentThread().getName()); } } }
public static void main(String[] args) { Runnable runnable = new SaleTicketThread(); Thread saleTicketThread1 = new Thread(runnable); Thread saleTicketThread2 = new Thread(runnable); Thread saleTicketThread3 = new Thread(runnable); saleTicketThread1.start(); saleTicketThread2.start(); saleTicketThread3.start(); }
因为3个线程都是异步执行的,因此每次的运行结果可能是不一样,以下列举2次不同的运行结果。
第1次运行结果:
10 is saled by Thread-0
8 is saled by Thread-0
7 is saled by Thread-0
5 is saled by Thread-0
9 is saled by Thread-1
3 is saled by Thread-1
2 is saled by Thread-1
1 is saled by Thread-1
4 is saled by Thread-0
6 is saled by Thread-2
第2次运行结果:
10 is saled by Thread-0
9 is saled by Thread-0
8 is saled by Thread-0
7 is saled by Thread-0
6 is saled by Thread-0
5 is saled by Thread-0
3 is saled by Thread-0
2 is saled by Thread-0
4 is saled by Thread-2
1 is saled by Thread-1
如果将上面的SaleTicketThread修改成继承Thread类的方式,就变成了3个线程各自拥有10张票,即变成了30张票,而不是3个线程共享10张票。
2. Thread类start()和run()的区别
2.1 示例
因为实现Runnable接口的优势,基本上实现多线程都使用的是该种方式,所以我们将之前定义的MyFirstThread也修改为实现Runnable接口的方式:
package com.zwwhnly.springbootaction.javabase.thread;
public class MyFirstThread implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.printf("[MyFirstThread]输出:%d,当前线程名称:%s\n", i, Thread.currentThread().getName()); } } }
然后仍然沿用之前定义的MyFirstThread、MySecondThread,我们先看下调用start()的效果:
package com.zwwhnly.springbootaction.javabase.thread;
public class ThreadTest { public static void main(String[] args) { System.out.println("主线程开始执行,当前线程名称:" + Thread.currentThread().getName()); Thread firstThread = new Thread(new MyFirstThread()); Runnable target = new MySecondThread(); Thread secondThread = new Thread(target); firstThread.start(); secondThread.start(); System.out.println("主线程执行结束,当前线程名称:" + Thread.currentThread().getName()); } }
运行结果(注意:多次运行,结果可能不一样):
主线程开始执行,当前线程名称:main
[MyFirstThread]输出:0,当前线程名称:Thread-0
[MyFirstThread]输出:1,当前线程名称:Thread-0
[MySecondThread]输出:0,当前线程名称:Thread-1
主线程执行结束,当前线程名称:main
[MySecondThread]输出:1,当前线程名称:Thread-1
[MySecondThread]输出:2,当前线程名称:Thread-1
[MySecondThread]输出:3,当前线程名称:Thread-1
[MySecondThread]输出:4,当前线程名称:Thread-1
[MyFirstThread]输出:2,当前线程名称:Thread-0
[MyFirstThread]输出:3,当前线程名称:Thread-0
[MyFirstThread]输出:4,当前线程名称:Thread-0
可以看出,调用start()方法后,程序中有3个线程,分别为主线程main、Thread-0、Thread-1,而且执行顺序不是按顺序执行的,存在不确定性。
然后将start()方法修改为run()方法,如下所示:
firstThread.run();
secondThread.run();
此时的运行结果如下所示(多次运行,结果是一样的):
主线程开始执行,当前线程名称:main
[MyFirstThread]输出:0,当前线程名称:main
[MyFirstThread]输出:1,当前线程名称:main
[MyFirstThread]输出:2,当前线程名称:main
[MyFirstThread]输出:3,当前线程名称:main
[MyFirstThread]输出:4,当前线程名称:main
[MySecondThread]输出:0,当前线程名称:main
[MySecondThread]输出:1,当前线程名称:main
[MySecondThread]输出:2,当前线程名称:main
[MySecondThread]输出:3,当前线程名称:main
[MySecondThread]输出:4,当前线程名称:main
主线程执行结束,当前线程名称:main
可以看出,调用run()方法后,程序中只有一个主线程,自定义的2个线程并没有启动,而且执行顺序也是按顺序执行的。
1.2 总结
以下是重点,面试常问!
- run()方法只是一个普通方法,调用之后程序会等待run()方法执行完毕,所以是串行执行,而不是并行执行。
- start()方法会启动一个线程,当线程得到CPU资源后会自动执行run()方法体中的内容,实现真正的并发执行。
3. Runnable和Callable的区别
在文章前面的章节中(1.2 实现Runnable接口 和1.3 实现Callable接口),我们了解了如何使用Runnable、Callable接口来创建线程,现在我们分别看下Runable和Callable接口的定义,其中,Runable接口的定义如下所示:
public interface Runnable {
public abstract void run(); }
Callable接口的定义如下所示:
public interface Callable<V> { V call() throws Exception; }
由此可以看出,Runnable和Callable的区别主要有以下几点:
- Runable的执行方法是run(),Callable的执行方法是call()
- call()方法可以抛出异常,run()方法如果有异常只能在内部消化
- 实现Runnable接口的线程没有返回值,实现Callable接口的线程能返回执行结果
- 实现Callable接口的线程,可以和FutureTask一起使用,获取到线程是否完成、线程是否取消、线程执行结果,也可以取消线程的执行。