在 Java 开发中,多线程并发是一个永恒不变的话题与热点。这里我们开始讨论如何在开发中使用多线程实现并发

abstract.jpeg
Thread 类
在 Java 中实现多线程最简单的一个方式就是继承 Thread 类、重写 run 方法,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
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--; System.out.println(this.getName() + ":余票数量: " + num); } } }
|
直接构造 TicketWindow1 线程实例,然后通过 start 方法来启动该线程即可
1 2 3 4
| public static void test1() { new TicketWindow1("#1售票窗口").start(); new TicketWindow1("#2售票窗口").start(); }
|
从测试结果,我们可以看到两个售票窗口的线程被正确的启动、运行。由于两个售票线程是分别构造的,故也可以看出实际上两个线程之间是相互独立的,分别售票,即两个线程的 num 变量是相互独立的

figure 1.jpeg
对于这种通过继承 Thread 类实现多线程的方式,好处是我们可以直接在子类中通过 this 来获取当前线程,而无需通过 Thread.currentThread () 方法。但就目前来看缺点同样明显,由于 Java 不支持多继承,仅仅为了支持多线程就使用了一个继承资格显然有些浪费
Runnable 接口
那如果即不想浪费唯一的继承名额,又想实现多线程,那该怎么办呢?答案就是 Runnable 接口。通过实现 Runnable 接口的 run 方法同样可以达到并发的目的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
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 方法来启动该线程即可
1 2 3 4
| public static void test2() { new Thread(new TicketWindow2(), "#1售票窗口" ).start(); new Thread(new TicketWindow2(), "#2售票窗口" ).start(); }
|
从测试结果,我们可以看到两个售票窗口的线程被正确的启动、运行。同样地,这里两个售票的 Runnable 实例是分别构造的,故也可以看出实际上这里两个线程之间是同样相互独立的,分别售票,即两个线程的 num 变量是相互独立的

figure 2.jpeg
当然利用 Runnable 接口实现多线程不仅可以避免继承名额的浪费,还可以像下面的示例一样,利用同一个 Runnable 任务实例来分别创建多个线程,即多个线程共同处理同一个资源
1 2 3 4 5 6
| public static void test3() { Runnable ticketWindow2 = new TicketWindow2();
new Thread( ticketWindow2, "#1售票窗口" ).start(); new Thread( ticketWindow2, "#2售票窗口" ).start(); }
|

figure 3.png
这时从测试结果中我们可以看出,虽然两个售票窗口的线程被正确的启动、运行,但他们执行的是同一个 Runnable 任务示例。因此两个售票窗口所能出售的票是共有的,即两个售票窗口线程的 num 变量是共享的
Callable 接口
不论是通过 Thread 类还是通过 Runnable 接口的方式实现多线程,均存在有一个弊端 —— 任务没有返回值。为此 Java 在 1.5 版本中提供一个新的接口 ——Callable。其和 Runnable 接口类似,只不过其提供的不是 run 方法而是 call 方法,其可返回任务结果
1 2 3
| public interface Callable<V> { V call() throws Exception; }
|
下面即是一个实现 Callable 接口的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
public class TicketWindow3 implements Callable<String> { 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 方法执行结果进行了封装。测试代码如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public static void test4() {
TicketWindow3 ticketWindow3 = new TicketWindow3();
FutureTask<String> futureTask1 = new FutureTask<>(ticketWindow3); new Thread( futureTask1, "#1售票窗口" ).start();
FutureTask<String> 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 变量同样是共享的

figure 4.png
参考文献
- Java 并发编程之美 翟陆续、薛宾田著
v1.5.2