在Java开发中,多线程并发是一个永恒不变的话题与热点。这里我们开始讨论如何在开发中使用多线程实现并发
Thread类
在Java中实现多线程最简单的一个方式就是继承Thread类、重写run方法,如下所示
/*** 售票窗口,继承Thread类*/
public class TicketWindow1 extends Thread {
private int num;
public TicketWindow1(String name) {
super(name);
}
@Override
public void run() {
num = 10;
System.out.println("Thread [" + Thread.currentThread().getName() +"] 开始售票 ..." + "余票: " + num);
while (num>0) {
num--;
// 可直接使用this来获取当前线程 System.out.println(this.getName() + ":余票数量: " + num);
}
}
}
直接构造TicketWindow1线程实例,然后通过start方法来启动该线程即可
public static void test1() {
new TicketWindow1("#1售票窗口").start();
new TicketWindow1("#2售票窗口").start();
}
从测试结果,我们可以看到两个售票窗口的线程被正确的启动、运行。由于两个售票线程是分别构造的,故也可以看出实际上两个线程之间是相互独立的,分别售票,即两个线程的num变量是相互独立的
对于这种通过继承Thread类实现多线程的方式,好处是我们可以直接在子类中通过this来获取当前线程,而无需通过Thread.currentThread()方法。但就目前来看缺点同样明显,由于Java不支持多继承,仅仅为了支持多线程就使用了一个继承资格显然有些浪费
Runnable接口
那如果即不想浪费唯一的继承名额,又想实现多线程,那该怎么办呢?答案就是Runnable接口。通过实现Runnable接口的run方法同样可以达到并发的目的
/*** 售票窗口, 实现Runnable接口*/
public class TicketWindow2 implements Runnable{
private int num;
@Override
public void run() {
num = 10;
System.out.println("Thread [" + Thread.currentThread().getName() +"] 开始售票 ..." + "余票: " + num);
while (num>0) {
num--;
System.out.println(Thread.currentThread().getName() + ":余票数量: " + num);
}
}
}
类似地,我们将Runnable实例传入Thread实例,即可构造创建一个新的线程。然后通过start方法来启动该线程即可
public static void test2() {
new Thread(new TicketWindow2(), "#1售票窗口" ).start();
new Thread(new TicketWindow2(), "#2售票窗口" ).start();
}
从测试结果,我们可以看到两个售票窗口的线程被正确的启动、运行。同样地,这里两个售票的Runnable实例是分别构造的,故也可以看出实际上这里两个线程之间是同样相互独立的,分别售票,即两个线程的num变量是相互独立的
当然利用Runnable接口实现多线程不仅可以避免继承名额的浪费,还可以像下面的示例一样,利用同一个Runnable任务实例来分别创建多个线程,即多个线程共同处理同一个资源
public static void test3() {
Runnable ticketWindow2 = new TicketWindow2();
new Thread( ticketWindow2, "#1售票窗口" ).start();
new Thread( ticketWindow2, "#2售票窗口" ).start();
}
这时从测试结果中我们可以看出,虽然两个售票窗口的线程被正确的启动、运行,但他们执行的是同一个Runnable任务示例。因此两个售票窗口所能出售的票是共有的,即两个售票窗口线程的num变量是共享的
Callable接口
不论是通过Thread类还是通过Runnable接口的方式实现多线程,均存在有一个弊端——任务没有返回值。为此Java在1.5版本中提供一个新的接口——Callable。其和Runnable接口类似,只不过其提供的不是run方法而是call方法,其可返回任务结果
public interface Callable {
V call() throws Exception;
}
下面即是一个实现Callable接口的实例
/*** 售票窗口,实现Callable接口*/
public class TicketWindow3 implements Callable {
private int num;
@Override
public String call() {
num = 10;
System.out.println("Thread [" + Thread.currentThread().getName() +"] 开始售票 ..." + "余票: " + num);
while (num>0) {
num--;
System.out.println(Thread.currentThread().getName() + ":余票数量: " + num);
}
return Thread.currentThread().getName() + ":票已售完";
}
}
但是由于Callable接口没有继承Runnable接口,故我们是不能像前面那样直接将一个Callable实例丢入Thread构造器中。所以Java还提供了一个FutureTask类,其不仅实现了Runnable接口可以用于包装Callable实例,还对Callable的call方法执行结果进行了封装。测试代码如下所示
public static void test4() {
TicketWindow3 ticketWindow3 = new TicketWindow3();
// 创建异步任务 FutureTask futureTask1 = new FutureTask<>(ticketWindow3);
// 启动线程 new Thread( futureTask1, "#1售票窗口" ).start();
FutureTask futureTask2 = new FutureTask<>(ticketWindow3);
new Thread( futureTask2, "#2售票窗口" ).start();
try{
// 阻塞等待所有异步任务完成 while( !futureTask1.isDone() || !futureTask2.isDone() ) {
}
// 获取异步任务结果 String result1 = futureTask1.get();
String result2 = futureTask2.get();
System.out.println("result1: " + result1);
System.out.println("result2: " + result2);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
测试结果如下,可以看到我们可以通过FutureTask获取到其所包装的执行结果。同样地,由于这里两个线程执行的均是同一个Callable任务实例,故两个售票窗口线程的num变量同样是共享的
参考文献Java并发编程之美 翟陆续、薛宾田著