线程和可运行的
导航 并发编程 主题: ) |
任何计算机的 CPU 都被设计为在任何给定时间执行一项任务,但我们并行运行多个应用程序,一切都能完美地协调工作。这不仅仅是因为 CPU 在执行计算方面非常快,还因为 CPU 使用了一种巧妙的设备来将它们的时间分配到各种任务中。在计算机上调用的每个应用程序或任务都与 CPU 相关联,形成一个进程。因此,CPU 管理着各种进程,并在各个进程之间来回跳转,为每个进程提供一小部分时间和处理能力。这发生得非常快,以至于对普通计算机用户来说,它呈现出进程同时运行的假象。CPU 将时间分配到进程的能力称为多任务处理。
因此,如果我们在计算机上运行一个 Java 应用程序,我们实际上是在创建与 CPU 相关联的进程,该进程可以获得一小部分 CPU 时间。在 Java 术语中,这个主进程被称为守护进程或守护线程。但是,Java 更进一步。它允许程序员将这个守护线程分成多个线程,这些线程可以同时执行(非常像 CPU),从而为 Java 应用程序提供更精细的多任务处理能力,称为多线程。
在本节中,我们将了解线程是什么以及如何在 Java 程序中实现多线程,使其看起来协调一致并且响应速度快。
根据以上讨论,线程是操作系统可以调度的最小处理单元。因此,使用线程,程序员可以有效地创建两个或多个同时运行的任务[1]。第一个行动呼叫是实现一个特定线程将执行的一组任务。为此,我们需要创建一个Runnable
进程。
一个Runnable
进程块是一个简单的类,它实现了一个run()
方法。在run()
方法中是需要由运行的线程执行的实际任务。通过使用Runnable
接口实现一个类,我们确保该类包含一个run()
方法。考虑以下程序
代码清单 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()
方法。
代码部分 1.1:实现Runnable 接口
public class RunnableProcess implements Runnable {
...
public void run() {
...
}
}
|
然后我们声明类剩余的逻辑。对于构造函数,我们接受一个String
参数,它将用作类的名称。然后,我们用0
到999
之间的随机数初始化类成员变量time
。为了确保随机数的初始化,我们在java.util
包中使用了Random
类。
代码部分 1.2:包含生成0 到999 之间随机整数的能力
import java.util.Random;
...
private Random rand = new Random();
...
this.time = rand.nextInt(999);
|
该可运行块将执行的实际任务在run()
方法中给出。为了避免由于并发编程导致异常,我们将此方法中的代码用try..catch
块包装起来。执行的任务实际上只包含三个语句。第一个输出提供的可运行进程的名称,最后一个报告线程已执行。也许代码中最有趣的部分是第二个语句:Thread.sleep(...)
。
代码部分 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);
...
|
此语句允许执行当前可运行块的线程停止执行给定时间。这个时间以毫秒为单位给出。但为了方便起见,这个时间将是构造函数中生成的随机数,可以在0
到999
毫秒之间。我们将在后面的部分探讨这一点。创建Runnable
进程块仅仅是开始。没有代码实际执行。为此,我们需要创建线程,然后这些线程分别执行这个任务。
一旦我们有了Runnable
进程块,我们就可以创建各种线程,然后这些线程可以执行封装在这些块中的逻辑。Java 中的多线程能力是使用Thread
类来利用和操作的。因此,一个Thread
对象包含创建真正多线程程序所需的所有逻辑和设备。考虑以下程序
代码清单 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
进程对象的引用即可。在上面的例子中,我们将 RunnableProcess
类(我们在 代码清单 1 中创建)的类对象传递给 Thread
构造函数。但对于每个对象,我们赋予了不同的名称(例如,"Thread-1"
和 "Thread-2"
等)来区分这三个 Thread
对象。上面的示例只声明了 Thread
对象,还没有开始执行它们。
启动线程
[edit | edit source]现在,我们已经知道如何有效地创建一个 Runnable
进程块和一个执行它的 Thread
对象,我们需要了解如何启动创建的 Thread
对象。这再简单不过了。在这个过程中,我们将在 Thread
对象上调用 start()
方法,瞧,我们的线程将开始执行它们各自的进程任务。
代码清单 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();
}
}
|
上面的代码将启动所有三个声明的线程。这样,所有三个线程将开始逐一执行。但是,由于这是并发编程,并且我们为执行停止声明了随机时间,因此我们每个人的输出将有所不同。以下是在我们执行上述程序时收到的输出。
代码清单 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
的执行并没有按预期顺序进行。不是按 t1
–t2
–t3
的顺序,而是按 t1
–t3
–t2
的顺序执行。线程执行的顺序完全取决于操作系统,并且可能在每次执行程序时都会发生变化,因此多线程应用程序的输出难以预测和控制。有些人认为这是导致多线程编程及其调试复杂性的主要原因。但是,应该观察到,一旦线程使用 Thread.sleep(...)
函数进入休眠状态,就可以相当熟练地预测执行间隔和顺序。休眠时间最短的线程是 t2
("Thread-2"
),休眠时间为 189
毫秒,因此它首先被调用。然后调用 t1
,最后调用 t3
。
操作线程
[edit | edit source]可以说,线程的执行顺序在一定程度上通过 Thread.sleep(...)
方法进行了操作。Thread
类具有这样的静态方法,这些方法可以说会影响线程的执行顺序和操作。以下是 Thread
类中一些有用的静态方法。这些方法被调用时只会影响当前正在运行的线程。
方法 | 描述 |
---|---|
Thread.currentThread()
|
返回在任何给定时间正在执行的线程。 |
Thread.dumpStack()
|
打印当前正在运行线程的堆栈跟踪。 |
Thread.sleep(long millis)
|
暂停当前正在运行线程的执行,持续给定的时间(以毫秒为单位)。 抛出 InterruptedException |
Thread.sleep(long millis, int nanos)
|
暂停当前正在运行线程的执行,持续给定的时间(以毫秒加上提供的纳秒为单位)。 抛出 InterruptedException |
Thread.yield()
|
暂时暂停当前正在运行线程的执行,以允许其他线程执行。 |
同步
[edit | edit source]下面是一个创建和运行多个线程的示例,这些线程以同步的方式运行,以便当一个线程使用特定资源时,其他线程会等待,直到该资源被释放。我们将在后面的部分详细讨论这一点。
代码清单 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();
}
}
}
}
}
|
代码清单 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. |
线程在哪里使用?
[edit | edit source]线程在需要大量 CPU 使用的应用程序中被大量使用。对于那些耗时且密集的操作,通常建议使用线程。此类应用程序的示例将是典型的视频游戏。在任何给定时间,视频游戏都涉及各种角色、周围环境中的物体以及其他需要同时处理的细微差别。处理游戏中每个元素或物体都需要大量的线程来监控每个物体。
例如,请看右侧角色扮演策略游戏的屏幕截图。这里,游戏画面描绘了屏幕上各种游戏角色的移动。现在想象一下处理屏幕上可见的每个角色的移动、方向和行为。如果这是按顺序完成的一项一项任务,那么移动每个角色肯定需要很多时间。但是,如果使用多线程的基本原理,每个角色将与其他角色同步移动。
线程不仅在视频游戏中被大量使用,而且在从简单的浏览器应用程序到复杂的操作系统和网络应用程序的方方面面都很常见。今天,它往往超越了开发人员的简单偏好,而是需要最大程度地利用基于大量多任务处理的当代硬件。
参考
[edit | edit source]- ↑ 单个 Java 应用程序可以同时运行的任务数量取决于操作系统允许多少任务进行多线程。