Java 9 并发编程实战
上QQ阅读APP看书,第一时间看更新

1.2 线程的创建、运行和设置

本节介绍如何使用Java API对线程进行基本的操作。与Java语言中的基本元素一样,线程也是对象(Object)。在Java中,创建线程的方法有以下两种。

• 直接继承Thread类,然后重写run()方法。

• 构建一个实现Runnable接口的类并重写run()方法,然后创建该类的实例对象,并以其作为构造参数去创建Thread类的对象。建议首选这种方法,因为它可以带来更多的扩展性。

在本节中,我们将采用第二种方法创建线程,然后学习如何改变线程的属性。Thread类包含如下一些信息属性,它们能够辅助区分不同的线程、反映线程状态、控制其优先级等。

ID:该属性存储了每个线程的唯一标识符。

Name:该属性存储了线程的名字。

Priority:该属性存储了Thread对象的优先级。在Java 9中,线程优先级的范围为1~10,其中1表示最低优先级,10表示最高优先级。通常不建议修改线程的优先级。线程优先级仅供底层操作系统作为参考,不能保证任何事情,如果一定要修改,请知晓优先级仅仅代表了一种可能性。

Status:该属性保存了线程的状态。在Java中,线程有6种状态——Thread. State枚举中定义这些状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_ WAITING和TERMINATED。这些状态的具体意义如下。

❑ NEW:线程已经创建完毕但未开始执行。

❑ RUNNABLE:线程正在JVM中执行。

❑ BLOCKED:线程处于阻塞状态,并且等待获取监视器。

❑ WAITING:线程在等待另一个线程。

❑ TIMED_WAITING:线程等待另一个线程一定的时间。

❑ TERMINATED:线程执行完毕。

本节将在一个案例中创建10个线程来找出20000以内的奇数。

项目准备

本案例是用Eclipse IDE实现的。如果开发者使用Eclipse或者其他IDE(例如NetBeans),则应打开它并创建一个新的Java项目。

案例实现

根据如下步骤实现本案例。

1.创建一个名为Calculator的类,并实现Runnable接口:

        public class Calculator implements Runnable {

2.实现run()方法。在这个方法中,存放着线程将要运行的指令。在这里,这个方法用来计算20000以内的奇数:

        @Override
        public void run() {
          long current = 1L;
          long max = 20000L;
          long numPrimes = 0L;

          System.out.printf("Thread '%s': START\n",
                            Thread.currentThread().getName());
          while (current <= max) {
            if (isPrime(current)) {
              numPrimes++;
            }
            current++;
          }
          System.out.printf("Thread '%s': END. Number of Primes: %d\n",
                          Thread.currentThread().getName(), numPrimes);
        }

3.实现辅助方法isPrime()。该方法用于判断一个数是否为奇数:

        private boolean isPrime(long number) {
          if (number <= 2) {
            return true;
          }
          for (long i = 2; i < number; i++) {
            if ((number % i) == 0) {
              return false;
            }
          return true;
          }
        }

4.实现应用程序的主方法,创建包含main()方法的Main类:

        public class Main {
                public static void main(String[] args) {

5.首先,输出线程的最大值、最小值和默认优先级:

        System.out.printf("Minimum Priority: %s\n",
                          Thread.MIN_PRIORITY);
        System.out.printf("Normal Priority: %s\n",
                          Thread.NORM_PRIORITY);
        System.out.printf("Maximun Priority: %s\n",
                          Thread.MAX_PRIORITY);

6.创建10个Thread对象,分别用来执行10个Calculator任务。再创建两个数组,用来保存Thread对象及其State对象。后续我们将用这些信息来查看线程的状态。这里将5个线程设置为最大优先级,另5个线程设置为最小优先级:

        Thread threads[];
        Thread.State status[];
        threads = new Thread[10];
        status = new Thread.State[10];
        for (int i = 0; i < 10; i++) {
        threads[i] = new Thread(new Calculator());
          if ((i % 2) == 0) {
            threads[i].setPriority(Thread.MAX_PRIORITY);
          } else {
            threads[i].setPriority(Thread.MIN_PRIORITY);
          }
            threads[i].setName("My Thread " + i);
        }

7.接着将一些必要的信息保存到文件中,因此需要创建try-with-resources语句来管理文件。在这个代码块中,先将线程启动前的状态写入文件,然后启动线程:

        try (FileWriter file = new FileWriter(".\\data\\log.txt");
        PrintWriter pw = new PrintWriter(file);) {
          for (int i = 0; i < 10; i++) {
            pw.println("Main : Status of Thread " + i + " : " +
                        threads[i].getState());
            status[i] = threads[i].getState();
            }
            for (int i = 0; i < 10; i++) {
              threads[i].start();
            }

8.等待线程运行结束。在1.6节中,我们将用join()方法来等待线程结束。本案例中,由于我们需要记录线程运行过程中状态的转变,因此不能使用join()方法来等待线程结束,而应使用如下代码:

            boolean finish = false;
            while (!finish) {
              for (int i = 0; i < 10; i++) {
                if (threads[i].getState() != status[i]) {
                  writeThreadInfo(pw, threads[i], status[i]);
                  status[i] = threads[i].getState();
              }
                }

              finish = true;
              for (int i = 0; i < 10; i++) {
                finish = finish && (threads[i].getState() ==
                                  State.TERMINATED);
              }
            }
          } catch (IOException e) {
            e.printStackTrace();
          }
        }

9.在上述代码中,我们通过调用writeThreadInfo()方法来将线程信息记录到文件中。代码如下:

        private static void writeThreadInfo(PrintWriter pw,
                                              Thread thread,
                                              State state) {
          pw.printf("Main : Id %d - %s\n", thread.getId(),
                      thread.getName());
          pw.printf("Main : Priority: %d\n", thread.getPriority());
          pw.printf("Main : Old State: %s\n", state);
          pw.printf("Main : New State: %s\n", thread.getState());
          pw.printf("Main : ************************************\n");
        }

10.运行程序,然后观察不同的线程是如何同时运行的。

结果分析

下图是程序在控制台的输出,从中可以看到,线程正在并行处理各自的工作。

从下面的屏幕截图中可以看到线程是如何创建的,拥有高优先级的偶数编号线程比低优先级的奇数编号线程优先执行。该截图来自记录线程状态的log.txt文件。

每个Java应用程序都至少有一个执行线程。在程序启动时,JVM会自动创建执行线程运行程序的main()方法。

当调用Thread对象的start()方法时,JVM才会创建一个执行线程。也就是说,每个Thread对象的start()方法被调用时,才会创建开始执行的线程。

Thread类的属性存储了线程所有的信息。操作系统调度执行器根据线程的优先级,在某个时刻选择一个线程使用CPU,并且根据线程的具体情况来实现线程的状态。

如果没有指定线程的名字,那么JVM会自动按照Thread-XX格式为线程命名,其中XX是一个数字。线程的ID和状态是不可修改的,事实上,Thread类也没有实现setId()和setStatus()方法,因为它们会引入对ID和状态的修改。

一个Java程序将在所有线程完成准确来说,是所有非守护线程完成。——译者注后结束。初始线程(执行main()方法的线程)完成,其他线程仍会继续执行直到完成。如果一个线程调用System.exit()命令去结束程序,那么所有线程将会终止各自的运行。

创建一个Thread对象并不意味着会创建一个新的执行线程。同样,调用实现Runnable接口类的run()方法也不会创建新的执行线程。只有调用了start()方法,一个新的执行线程才会真正创建。

其他说明

正如本节开头所说,还有另一种创建执行线程的方法——实现一个继承Thread的类,并重写其run()方法,创建该类的对象后,调用start()方法即可创建执行线程。

可以使用Thread类的静态方法currentThread()来获取当前运行线程的Thread对象。

调用setPriority()方法时,需要对其抛出的IllegalArgumentException异常进行处理,以防传入的优先级不在合法范围内(1和10之间)。

参考阅读

• 1.11节