Java中的线程有三种创建方式:继承Thread类、实现Runnable接口和实现Callable接口。下面我们一一进行介绍。
一、继承Thread类
使用该方法创建线程有如下三步:
1. 自定义线程类继承Thread类;
2. 重写该类中的run()方法,编写线程执行体;
3. 创建线程对象,调用start()方法启动线程。
例如,我们创建3个线程,分别去寻找指定区间内的素数。
public class Main {
public static void main(String[] args) {
EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000);
EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000);
EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);
task1.start();
task2.start();
task3.start();
}
}
class EnumeratePrimeNumber extends Thread {
private int start;
private int end;
public EnumeratePrimeNumber() {}
public EnumeratePrimeNumber(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
for (int i = start; i < end; ++i) {
boolean isPrime = true;
for (int j = 2; j * j <= i; ++j) {
if (i % j == 0) {
isPrime = false;
break;
}
}
if (isPrime)
System.out.println(i);
}
}
}
该程序的核心点有:
? ? ? ? 重写了Thread类里的run()方法,从而可以寻找素数。
- 语句
EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000);
EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000);
EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);
? ? ? ? 分别创建了3个线程对象,分别寻找10000~11000、20000~21000和30000~31000间的素数。
????????调用start()方法启动这3个线程。
程序的运行结果如下所示:
10007
10009
30011
20011
30013
10037
10039
10061
30029
20021
30047
10067
10069
......
可以看到,程序的执行结果并非是按照10000~11000、20000~21000和30000~31000三个区间依次输出的,而是在这三个区间之间来回切换,但是每个区间内找到的素数依然还是有序的。
二、实现Runnable接口
使用该方法创建线程有如下三步:
1. 自定义线程类实现Runnable接口;
2. 重写该类中的run()方法,编写线程执行体;
3. 创建自定义线程类的对象,接着创建Thread线程类对象,并将自定义线程类的对象作为Thread线程类构造方法的传入参数,最后调用Thread线程类对象的start()方法启动线程。
具体的步骤其实和第一种线程创建方式类似,只是第一步不再是继承而是实现,第三步不是直接调用自定义线程类对象的start方法,而是通过Thread线程类对象进行代理。
依然以上面寻找素数的问题为例,使用Runnable接口的实现程序如下:
public class Main {
public static void main(String[] args) {
EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000);
EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000);
EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);
// can also be
// Thread t1 = new Thread(task1);
// t1.start();
new Thread(task1).start();
new Thread(task2).start();
new Thread(task3).start();
}
}
class EnumeratePrimeNumber implements Runnable {
private int start;
private int end;
public EnumeratePrimeNumber() {}
public EnumeratePrimeNumber(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
for (int i = start; i < end; ++i) {
boolean isPrime = true;
for (int j = 2; j * j <= i; ++j) {
if (i % j == 0) {
isPrime = false;
break;
}
}
if (isPrime)
System.out.println(i);
}
}
}
上面代码相比于之前的,最大的区别有:
由于Java只支持单继承,所以在实践中更推荐使用实现Runnable接口创建线程。除此之外,这样的方式还支持同一对象被多个线程所使用。例如,我们可以编写程序模拟“抢票”——由多个线程去使用同一资源。
public class Main {
public static void main(String[] args) {
SnapUp race = new SnapUp();
new Thread(race, "Alice").start();
new Thread(race, "Bob").start();
new Thread(race, "Cathy").start();
}
}
class SnapUp implements Runnable {
private static int numberOfTicket = 10;
@Override
public void run() {
while (true) {
if (numberOfTicket > 0) {
System.out.println(Thread.currentThread().getName() + " gets " + (11 - numberOfTicket) + " ticket");
numberOfTicket--;
} else {
break;
}
}
}
}
上述程序安排了3个线程去模拟3个人,抢购10张票,并输出抢购结果。运行结果如下:
Alice gets 1 ticket
Alice gets 2 ticket
Alice gets 3 ticket
Bob gets 1 ticket
Alice gets 4 ticket
Alice gets 6 ticket
Alice gets 7 ticket
Bob gets 5 ticket
Bob gets 9 ticket
Bob gets 10 ticket
Cathy gets 8 ticket
Alice gets 8 ticket
非常惊讶的是,第1张票被Alice和Bob两个人都抢到了,第8张票被Cathy和Alice两个人都抢到了,这显然是不正确的。专业上将其称之为多线程访问导致的数据不一致。
最后总结一下使用实现Runnable接口创建线程的好处:
1. 避免了Thread类单继承的局限,更加灵活方便;
2. 方便同一个对象被多个线程使用。
三、实现Callable接口
有时候我们希望返回每个线程的执行结果,或者希望线程可以抛出异常,那么我们可以使用实现Callable接口创建线程。
实现Callable接口来创建线程的步骤如下:
1.?自定义线程类实现Callable接口;
2. 重写该类中的call()方法,编写线程执行体,给出返回值并抛出异常;
3. 创建自定义线程类的对象,比如
? ? Task t = new Task();
4. 创建执行服务,比如
? ? ExecuteService es = Executors.newFixedThreadPool(3); // 这里创建了大小为3的定长线程池
5. 提交执行,比如
? ? Future<Integer> futureResult = es.submit(t); // 这里的返回值为Integer类
6. 获取结果,比如
? ? Integer result =?futureResult.get();
7. 关闭服务
? ? es.shutdownNow();
这里我们以并行求和为例,即利用多个线程分别求出arr[s] + arr[s+1] + ... + arr[e-2] + arr[e-1]的和(或者说下标s~e-1间所有数组元素之和)。
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
int[] arr = new int[30000];
for (int i = 0; i < 30000; i++) {
arr[i] = i;
}
Summation task1 = new Summation(arr, 0, 10000);
Summation task2 = new Summation(arr, 10000, 20000);
Summation task3 = new Summation(arr, 20000, 30000);
ExecutorService es = Executors.newFixedThreadPool(3);
Future<Integer> futureSum1 = es.submit(task1);
Future<Integer> futureSum2 = es.submit(task2);
Future<Integer> futureSum3 = es.submit(task3);
int sum1 = futureSum1.get();
int sum2 = futureSum2.get();
int sum3 = futureSum3.get();
System.out.println("Sum = " + (sum1 + sum2 + sum3));
es.shutdown();
}
}
class Summation implements Callable<Integer> {
private int[] arr;
private int start;
private int end;
public Summation() {}
public Summation(int[] arr, int start, int end) {
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
public Integer call() throws ArrayIndexOutOfBoundsException {
int sum = 0;
for (int i = start; i < end; ++i) {
sum += arr[i];
}
System.out.println(Thread.currentThread().getName() + ": " + sum);
return sum;
}
}
上述程序的运行结果为
pool-1-thread-2: 149995000
pool-1-thread-3: 249995000
pool-1-thread-1: 49995000
Sum = 449985000
|