本文版权归作者所有,转载请标明出处。未与作者联系不得用于任何商业用途
2011年5月18日
关新全
山东东营 石油大学
第二章:java多线程
-----尽信书不若无书----
多线程在实际项目中应用十分广泛,例如,web服务使用一个线程等待接收用户请求,而使用其他子线程处理请求;Swing图形界面中使用一个线程接收界面的响应,开启其他线程处理响应引发的动作。有时开启多个线程还可能提高多核计算机的利用率,从而提高程序的计算性能。此外,java线程中遇到未捕获异常,线程将会终止运行,如果应用程序中未开启子线程,则主线程遭遇未捕获异常时将会异常退出,导致整个应用程序的崩溃退出,而使用多线程,其中某个线程遇到未捕获异常退出时,整个应用程序仍然可以继续运行。一般来说java多线程技术主要是提高应用程序的响应能力,而对于提高计算能力是有限的(C/C++更适合编写高性能计算的程序,java更适合编写网络应用),而且线程的创建和运行会消耗系统资源,在java应用程序中开启过多的线程,往往会严重影响应用程序的执行效率。
由于虚拟计算平台本身是基于网络的应用,需要使用大量与多线程相关的知识,灵活的使用多线程技术能够使程序的框架更加清晰,减少用户的请求和响应时间,减少系统因出错而导致崩溃的几率。所以我们将多线程技术放在了最前面进行讲解。
本章将从线程创建,线程同步,线程池,锁机制,线程安全集合和线程异常5个方面对java多线程技术进行分析。线程创建阐述如何创建线程和创建不同线程的方法和联系;线程同步主要考虑如何协调多个线程的执行进度;使用线程池能够对多个线程进行调度;锁机制探讨了关于锁相关的概念和具体应用;线程安全集合关注多线程程序中应当使用的集合类,最后给出线程中可能的异常,以及异常的处理方式。
2.1 线程创建
Java通过继承Thread类、实现Runnable接口、实现Callable接口三种方式创建一个新线程。线程在创建完成之后,处于新生状态,需要调用Thread的start方法来开启执行一个线程。
2.1.1 继承Thread类
可以通过继承Thread类,创建一个新的线程。重写Thread类的run方法将子线程的执行逻辑放入,调用Thread类的start方法开启这个新线程。需要注意的是,调用Thread类的run方法并不会创建一个新的线程执行run内部的逻辑代码,相反,会在调用线程中执行run逻辑。要想开启新的线程,必须使用Thread类的start方法,稍后我们会给出相应的解释。
Demo2-1给出了一个通过重写Thread run方法开启一个线程的例子,在这个例子中,主线程首先创建5个子线程,然后等待一段时间,以便所有的子线程都能够成功的执行完成;每个子线程将会打印出当前线程的名字。线程的执行次序往往是不可预知的,因此输出的结果次序也是不可预知的。
Demo2-1 继承Thread类,重写run方法,创建一个新线程
package com.upc.upcgrid.guan.advancedJava.chapter02;
public class ThreadCreateThread {
public static final int MAX_THREAD_SIZE = 5;
public static void main(String[] args) throws InterruptedException {
for(int i = 0 ; i < MAX_THREAD_SIZE ; i++)
{
new Thread(){
public void run() {
System.out.println(Thread.currentThread().getName());
};
}.start();
}
System.out.println(Thread.currentThread().getName());
}
}
Demo2-1 执行结果:
Thread-0
Thread-1
Thread-3
main
Thread-2
Thread-4
2.1.2 实现Runnable接口
可以通过实现Runnable接口的方式创建一个新的线程,Runnable接口中仅有一个run方法,需要将子线程的执行逻辑放入run方法中。通过调用Thread类的start方法启动这个新线程。
Demo2-2给出了使用Runnable接口创建和开启新线程的例子。这个例子与Demo2-1的功能相同。实现了Runnable接口的类实例经过Thread类包装之后,才能调用Thread的start方法,开启一个新线程执行run体内内容。Thread类可以接收一个Runnable类型的参数,并使用Runnable的实例开启新线程。
Demo2-2 使用Runnable接口开启新线程
package com.upc.upcgrid.guan.advancedJava.chapter02;
public class RunnableCreateThread {
public static final int MAX_THREAD_SIZE = 5;
public static void main(String[] args) throws InterruptedException {
for(int i = 0 ; i < MAX_THREAD_SIZE ; i++)
{
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
}
System.out.println(Thread.currentThread().getName());
}
}
2.1.3 实现Callable接口
继承Thread方法或者实现Runnable接口两种方式创建的多线程,线程逻辑执行结束后都没有返回值,因为run方法的返回值是void。而使用Callable接口可以提供一个类型参数,在线程结束后可以将线程的执行结果反馈回来。Callable接口仅提供一个方法call,这个方法有一个模板类型的返回值,返回值类型需要在创建Callable实例时指定。要想使用Callable接口创建执行新线程,需要将线程的执行逻辑放入call方法中,将线程执行的结果作为call方法的返回值返回。Thread类不能直接包装Callable接口,Thread类实现了Runnable接口但却未实现Callable接口。Callable接口需要借助FutureTask类对Callable进行包装。FutureTask类实现类Runnable接口,并且能够包装Callable类型的实例。使用FutureTask的get方法可以阻塞式等待Callable线程执行结束,并能够获取到子线程的返回值。
Demo2-3给出了使用Callable创建多线程的例子,这个例子完成了0-99这100个数求和,主线程开启10个子线程,每个线程负责计算其中的10个数的和,最终主线程统计计算结果并将最终结果输出。主线程创建实现了Callable接口的实例,然后使用FutureTask对Callable接口进行包装,在使用Thread类对FutureTask进行包装(实际Thread是对FutureTask类的Runnable接口进行包装),然后通过Thread的start方法开启这个新线程。在主线程的末尾通过循环调用FutureTask的get方法,获取10个子线程的计算的临时结果,统计最终结果。需要注意的是FutureTask的get方法是阻塞式等待线程结束返回结果。就是说,当主线程调用其中一个线程的get方法时,如果这个线程尚未开始执行,或执行尚未结束,那么主线程将会阻塞等待此线程结束返回结果(这是一个同步的概念)。
Demo2-3 使用Callable创建多线程
package com.upc.upcgrid.guan.advancedJava.chapter02;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class OnceCompute implements Callable<Integer>{
private int start;//计算的起始数字
private int end;//计算的终止数字
public OnceCompute(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = start; i < end ; i++)
sum += i;
return sum;
}
}
public class CallableCreateThread {
public static final int MAX_THREAD_SIZE = 10;
public static void main(String[] args) throws InterruptedException, ExecutionException {
List<FutureTask<Integer>> tasks = new ArrayList<FutureTask<Integer>>();
int sum = 0;
for(int i = 0 ; i < MAX_THREAD_SIZE ; i++)//开始十个线程
{
FutureTask<Integer> task = new FutureTask<Integer>(new OnceCompute(i*10, (i+1)*10));
tasks.add(task);
new Thread(task).start();
}
for(FutureTask<Integer> task : tasks)//汇总结果
sum += task.get();//get阻塞等待线程执行结束
System.out.println(sum);
}
}
Demo2-3执行结果:
4950
2.1.4 FutureTask类的猜想
FutureTask可以包装一个Callable接口,同时也可以包装一个Runnable接口,并且能够提供额外的对线程的操作,例如:get方法可以阻塞式等待线程逻辑结束,可以用作线程同步,cancel方法用于取消线程执行,isDone判断线程是否执行结束等(更多操作请查看java API文档)。对于Callable来说,FutureTask可以获取返回值并完成同步,对于Runnable来说,FutureTask也可以完成同步,而且FutureTask带附加对线程有用的操作,因此在某些场合您可能会用到这个类。
Demo2-4给出了一个关于FutureTask类的猜测,MyFutureTask类实现了Runnable接口,并且可以包装Runnable接口和Callable接口,提供对线程状态的简单查询,同时能够阻塞式等待线程执行结束。(注意,这只是猜测,要想看真正的源码,请到官网上查询,这里只是为了方便大家理解)。
在Demo2-4中使用了synchronized来完成阻塞等待,这种锁机制将会在后续内容中给出。当调用get方法时,如果线程执行尚未结束,那么调用线程会阻塞,当线程执行结束后调用了notifyAll方法,此时调用线程会在wait处被唤醒,继续执行并返回结果。
将Demo2-3中的FutureTask替换成Demo2-4中的MyFutureTask,可以看出仍然能计算出正确的结果。
Demo2-4 FutureTask类的猜测
package com.upc.upcgrid.guan.advancedJava.chapter02;
import java.util.concurrent.Callable;
public class MyFutureTask<T> implements Runnable{
private Runnable runnable = null;
private Callable<T> callable = null;
private T ret = null;
private boolean isDone = false;
public MyFutureTask(Callable<T> callable) {
this.callable = callable;
}
public MyFutureTask(Runnable runnable) {
this.runnable = runnable;
}
@Override
public synchronized void run() {
if(runnable != null)
runnable.run();
if(callable != null)
try {
ret = callable.call();//注意run方法不能向外抛出异常的
} catch (Exception e) {}
isDone = true;
notifyAll();
}
public synchronized T get() throws InterruptedException
{
while(!isDone)
wait();
return ret;
}
public boolean isDone()
{
return isDone;
}
}
2.1.5 Thread、Runnable、Callable的联系与区别
类之间的关系如下图:

图2-1 Thread、Runnable、Callable类之间关系
从图中可以看出,Thread和FutureTask都实现了Runnable接口,Callable没有实现Runnable接口,但是FutureTask可以包装一个Callable接口,包装后的Callable接口就可以像Runnable接口一样使用。同样的,FutureTask也可以包装一个Runnable接口,只不过调用get方法最终会返回一个空值(或者一个指定的值,细节请查看API文档)。使用FutureTask包装Runnable和Callable是很常见的,应为FutureTask附带了一些对执行线程控制的方法,可以随时查询线程状态甚至可以终止一个线程。
至于在何时使用何种方式创建线程,这与个人风格和喜好有一定关系。但原则上这三种方式有一些略微的差别。首先,Thread是一个类,如果一个类已经继承自一个父类,那么这个类将无法继承Thread类,这时可以考虑使用Runnable和Callable。其次,如果线程的执行结果没有返回值,那么根本没有实现Callable接口的必要。最后如果需要线程返回参数,可以考虑使用Callable接口,但这不是必须的,因为在实现Runnable接口的类中,添加一个类成员属性来记录计算的结果,在线程执行结束后访问相应成员属性,依然可以获取线程执行的结果。但是如果想要创建一个无名的,带有返回值的线程类的对象,恐怕只有Callable可以胜任了。
FutureTask类提供了对Runnable和Callable的包装,并能够控制线程的执行,因此如果使用这个类能为您带来方便时,就尽量使用他。
2.1.6 Thread类的猜想
要想开启一个新线程,必须调用Thread类的start方法才可以,仅仅调用run方法无法开启一个新线程。Java为什么这样设计呢?这主要是start方法在执行线程逻辑代码前,需要创建线程资源,在逻辑代码结束后需要清理线程残余资源。
从Demo2-5可以看出,Thread类可以接收一个Runnable实例,并调用相应实例的run方法,当你重写Thread类的run方法时,子线程就会执行你提供的计算逻辑。直接调用run方法不能直接创建线程,调用start方法时,start方法先做一些线程初始化工作,然后执行线程run方法的逻辑,最后清理线程残余资源,这大概就是为什么需要通过start方法才能开启线程的原因。
Demo2-5 对Thread类内部的猜测
package com.upc.upcgrid.guan.advancedJava.chapter02;
public class MyThread implements Runnable{
private Runnable runnable = null;
public MyThread() {
}
public MyThread(Runnable runnable) {
this.runnable = runnable;
}
public void doSomethingBeforeThread()
{
//allocate thread and init it
}
public void doSomethingAfterThread()
{
//destroy thread and release resources
}
public void start()
{
doSomethingBeforeThread();
run();
doSomethingAfterThread();
}
@Override
public void run() {
if(this.runnable != null)
runnable.run();
}
}
}