跳转到内容

线程和可运行对象

75% developed
来自维基教科书,开放世界中的开放书籍
(从 Java:线程 重定向)

导航 并发编程 主题: v  d  e )


任何计算机的 CPU 都设计为在任何给定时间执行一项任务,但我们同时运行多个应用程序,一切都完美地协调一致。这不仅仅是因为 CPU 在执行计算方面非常快,而是因为 CPU 使用了一种巧妙的机制,将时间分配给各种任务。在计算机上调用的每个应用程序或任务都与 CPU 关联,形成一个进程。因此,CPU 管理着各种进程,并在每个进程之间来回跳转,给它一小部分时间和处理能力。这发生得如此之快,以至于对普通计算机用户来说,感觉多个进程在同时运行。CPU 将时间分配给多个进程的能力称为多任务处理

因此,如果我们在计算机上运行一个 Java 应用程序,我们实际上是在与 CPU 创建一个进程,该进程获得一小部分 CPU 时间。在 Java 的术语中,这个主进程被称为守护进程守护线程。但是,Java 更进一步。它允许程序员将这个守护线程划分为多个线程,这些线程同时执行(类似于 CPU),因此为 Java 应用程序提供了更精细的多任务处理能力,称为多线程

在本节中,我们将了解线程是什么以及如何在 Java 程序中实现多线程,使其看起来协调一致且响应速度快。

线程

[edit | edit source]

鉴于上述讨论,线程是操作系统可以调度的最小的处理单元。因此,使用线程,程序员可以有效地创建两个或多个任务[1],这些任务同时运行。第一个行动呼叫是实现一组任务,这些任务由特定线程执行。为此,我们需要创建一个Runnable 进程。

创建可运行的进程块

[edit | edit source]

Runnable 进程块是一个简单的类,它实现了一个run() 方法。在run() 方法中是需要由运行的线程执行的实际任务。通过实现使用 Runnable 接口的类,我们确保该类包含一个run() 方法。考虑以下程序

Computer code 代码清单 1: 可运行的进程
import java.util.Random;
public class RunnableProcess implements Runnable {
    private String name;
    private int time;
    private Random rand = new Random();

    public RunnableProcess(String name) {
        this.name = name;
        this.time = rand.nextInt(999);
    }

    public void run() {
        try {
            System.out.printf("%s is sleeping for %d \n", this.name, this.time);
            Thread.sleep(this.time);
            System.out.printf("%s is done.\n", this.name);
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}

在上面的代码中,我们创建了一个名为RunnableProcess 的类,并实现了Runnable 接口,以确保我们在类声明中有一个run() 方法。

Example 代码节 1.1: 实现Runnable 接口
public class RunnableProcess implements Runnable {
    ...
    public void run() {
        ...
    }
}

然后我们声明该类的其余逻辑。对于构造函数,我们使用一个String 参数,该参数用作类的名称。然后,我们将类成员变量time 初始化为0999 之间的随机数。为了确保随机数的初始化,我们在java.util 包中使用Random 类。

Example 代码节 1.2: 包含在0999 之间生成随机整数的能力
import java.util.Random;
...
private Random rand = new Random();
...
this.time = rand.nextInt(999);

由该可运行块执行的实际任务在run() 方法中呈现。为了防止由于并发编程而发生的异常,我们将此方法中的代码用try..catch 块括起来。执行的任务实际上只包含三个语句。第一个输出可运行进程的提供的名称,最后一个报告线程已执行。也许代码中最有趣的的部分是第二个语句:Thread.sleep(...)

Example 代码节 1.3: 实际的可运行进程任务
...
System.out.printf("%s is sleeping for %d \n", this.name, this.time);
Thread.sleep(this.time);
System.out.printf("%s is done \n", this.name);
...

此语句允许执行当前可运行块的线程停止其执行,持续给定时间。此时间以毫秒为单位。但为了我们的方便,此时间将是构造函数中生成的随机数,可以在0999 毫秒之间。我们将在后面的部分探讨这一点。创建Runnable 进程块仅仅是开始。实际上没有代码执行。为此,我们需要创建线程,然后这些线程将分别执行此任务。

创建线程

[edit | edit source]

一旦我们有了Runnable 进程块,我们就可以创建各种线程,这些线程可以执行这些块中包含的逻辑。Java 中的多线程能力是使用 Thread 类利用和操纵的。因此,Thread 对象包含创建真正的多线程程序所需的所有逻辑和设备。考虑以下程序

Computer code 代码清单 2: 创建Thread 对象
public class ThreadLogic {
    public static void main(String[] args) {
        Thread t1 = new Thread(new RunnableProcess("Thread-1"));
        Thread t2 = new Thread(new RunnableProcess("Thread-2"));
        Thread t3 = new Thread(new RunnableProcess("Thread-3"));
    }
}

创建线程与上面程序所示一样简单。您只需创建一个Thread 类的对象,并将对Runnable 进程对象的引用传递给它。在上面的例子中,我们将Thread 构造函数与我们在 代码清单 1 中创建的RunnableProcess 类的类对象一起使用。但对于每个对象,我们都会给出不同的名称(即,"Thread-1""Thread-2" 等等),以区分三个Thread 对象。上面的示例只声明了Thread 对象,还没有启动它们执行。

启动线程

[edit | edit source]

现在,我们知道了如何有效地创建一个Runnable 进程块和一个执行它的Thread 对象,我们需要了解如何启动创建的Thread 对象。这再简单不过了。对于此过程,我们将调用Thread 对象上的start() 方法,瞧,我们的线程将开始执行它们各自的进程任务。

Computer code 代码清单 3: 启动Thread 对象
public class ThreadLogic {
    public static void main(String[] args) {
        Thread t1 = new Thread(new RunnableProcess("Thread-1"));
        Thread t2 = new Thread(new RunnableProcess("Thread-2"));
        Thread t3 = new Thread(new RunnableProcess("Thread-3"));

        t1.start();
        t2.start();
        t3.start();
    }
}

以上代码将启动所有三个声明的线程。这样,所有三个线程将逐个开始执行。但是,由于这是并发编程,并且我们为执行停止声明了随机时间,因此我们每个人的输出都会有所不同。以下是我们在执行上述程序时收到的输出。

Computer code 代码清单 3 的输出
Thread-1 is sleeping for 419
Thread-3 is sleeping for 876
Thread-2 is sleeping for 189
Thread-2 is done
Thread-1 is done
Thread-3 is done

需要注意的是,Thread 的执行并没有按照预期顺序进行。线程不是按照 t1t2t3 的顺序执行的,而是按照 t1t3t2 的顺序执行的。线程执行的顺序完全取决于操作系统,并且在每次执行程序时都可能发生变化,因此多线程应用程序的输出难以预测和控制。有些人认为这是导致多线程编程及其调试复杂化的主要原因。但是,应该观察到,一旦使用 Thread.sleep(...) 函数将线程置于休眠状态,就可以相当可靠地预测执行间隔和顺序。睡眠时间最少的线程是 t2 ("Thread-2"),睡眠时间为 189 毫秒,因此它首先被调用。然后调用 t1,最后调用 t3

操作线程

[编辑 | 编辑源代码]

可以说,线程的执行顺序在一定程度上是通过使用 Thread.sleep(...) 方法来控制的。Thread 类具有这样的静态方法,这些方法可以说会影响线程的执行顺序和操作。以下是 Thread 类中一些有用的静态方法。这些方法在被调用时只影响当前正在运行的线程。

方法 描述
Thread.currentThread() 返回任何给定时间正在执行的线程。
Thread.dumpStack() 打印当前正在运行线程的堆栈跟踪。
Thread.sleep(long millis) 停止当前正在运行的线程执行给定时间(以毫秒为单位)。
抛出 InterruptedException
Thread.sleep(long millis, int nanos) 停止当前正在运行的线程执行给定时间(以毫秒加上提供的纳秒为单位)。
抛出 InterruptedException
Thread.yield() 暂时暂停当前正在运行的线程的执行,以允许其他线程执行。

下面是一个创建和运行多个线程的示例,这些线程以同步的方式运行,以便当一个线程使用特定资源时,其他线程会等待直到该资源被释放。我们将在后面的部分详细介绍这一点。

Computer code 代码清单 4:创建以同步方式运行的多个 Thread 对象
public class MultiThreadExample {
    public static boolean cthread;
    public static String stuff = " printing material";

    public static void main(String args[]) {
        Thread t1 = new Thread(new RunnableProcess());
        Thread t2 = new Thread(new RunnableProcess());
        t1.setName("Thread-1");
        t2.setName("Thread-2");
        t2.start();
        t1.start();
    }
    /*
     * Prints information about the current thread and the index it is
     * on within the RunnableProcess
     */
    public static void printFor(int index) {
        StringBuffer sb = new StringBuffer();
        sb.append(Thread.currentThread().getName()).append(stuff);
        sb.append(" for the ").append(index).append(" time.");
        System.out.print(sb.toString());
    }
}
class RunnableProcess implements Runnable {
    public void run() {
        for(int i = 0; i < 10; i++) {
            synchronized(MultiThreadExample.stuff) {
                MultiThreadExample.printFor(i);
                try {
               	    MultiThreadExample.stuff.notifyAll();
                    MultiThreadExample.stuff.wait();
                } catch(InterruptedException ex) {
                   ex.printStackTrace();
                }
            }
        }
    }
}
Computer code 代码清单 4 的输出
Thread-1 printing material for the 0 time.
Thread-2 printing material for the 0 time.
Thread-1 printing material for the 1 time.
Thread-2 printing material for the 1 time.
Thread-1 printing material for the 2 time.
Thread-2 printing material for the 2 time.
Thread-1 printing material for the 3 time.
Thread-2 printing material for the 3 time.
Thread-1 printing material for the 4 time.
Thread-2 printing material for the 4 time.
Thread-1 printing material for the 5 time.
Thread-2 printing material for the 5 time.
Thread-1 printing material for the 6 time.
Thread-2 printing material for the 6 time.
Thread-1 printing material for the 7 time.
Thread-2 printing material for the 7 time.
Thread-1 printing material for the 8 time.
Thread-2 printing material for the 8 time.
Thread-1 printing material for the 9 time.
Thread-2 printing material for the 9 time.

线程在哪些地方使用?

[编辑 | 编辑源代码]
视频游戏大量使用线程

线程在需要大量 CPU 使用量的应用程序中被大量使用。对于耗时且密集的操作,通常建议使用线程。此类应用程序的一个例子是典型的视频游戏。在任何给定的时间,视频游戏都涉及各种角色、周围环境中的物体以及其他需要同时处理的细微差别。处理游戏中的每个元素或物体都需要大量的线程来监控每个物体。

例如,以右侧的角色扮演策略游戏的屏幕截图为例。这里,游戏画面描绘了屏幕上移动的各种游戏角色。现在想象一下处理屏幕上每个可见角色的移动、方向和行为。如果要一个接一个地完成这项任务,那么移动每个角色肯定会花费很多时间。但是,如果使用多线程的基本原理,每个角色将以与其他角色同步的方式移动。

线程不仅在视频游戏中被大量使用,而且在从简单的浏览器应用程序到复杂的 operating systems 和网络应用程序的各个方面都普遍使用。如今,它往往超越了开发人员的简单偏好,而是成为了最大限度地利用依赖于繁重的多任务处理的现代硬件的必要性。

参考资料

[编辑 | 编辑源代码]
  1. 单个 Java 应用程序可以同时运行的任务数量取决于操作系统允许多线程运行的任务数量。

守护线程教程


Clipboard

待办事项
添加一些类似于 变量 中的练习


华夏公益教科书