Java并发之初识多线程开发

在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
/**
* 售票窗口,继承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方法来启动该线程即可

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
 /**
* 售票窗口, 实现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方法来启动该线程即可

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
/**
* 售票窗口,实现Callable接口
*/
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

参考文献

  1. Java并发编程之美 翟陆续、薛宾田著
0%