Java多线程编程——线程创建

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);
        }
    }
}

该程序的核心点有:

  • 语句
    class EnumeratePrimeNumber extends Thread
    定义了任务类EnumeratePrimeNumber,它继承了Thread类。
  • 语句
    @Override
    public void run() {...}

        重写了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间的素数。

  • 语句
    task1.start();
    task2.start();
    task3.start();

        调用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);
        }
    }
}

上面代码相比于之前的,最大的区别有:

  • 自定义任务类时不再继承Thread类,而是实现Runnable接口:
    class EnumeratePrimeNumber implements Runnable
  • 在启动线程时,需要先创建任务类的实例,并在创建Thread对象时作为参数传递,再调用start()方法启动:
    new Thread(task1).start();

由于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

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值