当前位置: 首页 > 图灵资讯 > 技术篇> Java 程序中的多线程

Java 程序中的多线程

来源:图灵教育
时间:2024-02-25 14:02:24

为什么要排队? 下面这个简单 Java 完成四项不相关任务的程序。这样的程序有一个控制线程,在这四个任务之间线性移动。此外,由于所需的资源 — 打印机、磁盘、数据库和显示屏 -- 由于硬件和软件的限制有内在的潜伏时间,所以每项任务都包含明显的等待时间。因此,在访问数据库之前,程序必须等待打印机完成打印文件等。如果你在等待程序的完成,这是对计算资源和时间的不良使用。改进这个程序的一种方法是使它成为多线程。

四项不相关任务

class myclass { static public void main(String args[]) { print_a_file(); manipulate_another_file(); access_database(); draw_picture_on_screen(); } }

在这种情况下,每项任务都必须等待前一项任务完成,即使涉及的任务无关紧要。然而,在现实生活中,我们经常使用多线程模型。我们还可以让孩子、配偶和父母在处理某些任务时完成其他任务。例如,当我写信时,我可能会把儿子送到邮局买邮票。软件术语称为多个控制(或执行)线程。

多个控制线程可以通过两种不同的方法获得:

在大多数操作系统中,可以创建多个过程。当一个程序启动时,它可以为即将到来的任务创建一个过程,并允许它们同时运行。当一个程序因等待网络访问或用户输入而被阻塞时,另一个程序也可以运行,从而提高资源利用率。然而,以这种方式创建每个过程需要付出一定的代价:设置一个过程需要占用相当多的处理器时间和内存资源。此外,大多数操作系统不允许进程访问其他过程的内存空间。因此,过程之间的通信非常不方便,也不会为简单的编程模型提供自己。 线程线程也称为轻型过程 (LWP)。由于线程只能在单个过程的作用范围内移动,因此创建线程比创建过程便宜得多。这样,线程比过程更可取,因为线程允许合作和数据交换,而且在计算资源方面非常便宜。线程需要操作系统的支持,因此并非所有机器都提供线程。Java 编程语言作为一种相当新的语言,将线程支持与语言本身相结合,为线程提供了强有力的支持。

使用 Java 编程语言实现线程 Java 编程语言使多线程如此简单有效,以至于一些程序员说它实际上是自然的。尽管在 Java 线程的使用比其他语言要容易得多,仍有一些概念需要掌握。要记住的一件重要的事情是 main() 函数也是一个线程,可以用来做有用的工作。只有在需要多个线程时,程序员才需要创建新的线程。

Thread 类 Thread 类是一个特定的类,即不是抽象类,这种包装线程的行为。程序员必须创建一个线程 Thread 类导出的新类。必须覆盖程序员 Thread 的 run() 函数完成有用的工作。用户不直接调用此函数,但必须调用 Thread 的 start() 再次调用函数,函数 run()。下面的代码说明了它的用法:

创建两个新的线程

import java.util.*; class TimePrinter extends Thread { int pauseTime; String name; public TimePrinter(int x, String n) { pauseTime = x; name = n; } public void run() { while(true) { try { System.out.println(name + ":" + new Date(System.currentTimeMillis())); Thread.sleep(pauseTime); } catch(Exception e) { System.out.println(e); } } } static public void main(String args[]) { TimePrinter tp1 = new TimePrinter(1000, "Fast Guy"); tp1.start(); TimePrinter tp2 = new TimePrinter(3000, "Slow Guy"); tp2.start(); } }

在这种情况下,我们可以看到一个简单的程序,它有两个不同的时间间隔(1 秒和 3 秒)显示屏幕上的当前时间。这是通过创建两个新的线程来完成的,包括 main() 共三个线程。然而,由于有时作为线程运行的类别可能是某一类别的一部分,因此不能再按照这种机制创建线程。虽然任何数量的接口都可以在同一类中实现,但是 Java 编程语言只允许一个类有一个父类。与此同时,一些程序员避免从 Thread 类导出,因为它强加了类级。对于这种情况,有必要 runnable 接口。

Runnable 接口 该接口只有一个函数,run()这个函数必须通过实现这个接口的类来实现。然而,就操作而言,它的语义与前一个例子略有不同。我们可以使用它 runnable 接口重写前一个示例。(不同部分用黑体表示。)

在不强加类级的情况下,创建两个新线程

import java.util.*; class TimePrinter implements Runnable { int pauseTime; String name; public TimePrinter(int x, String n) { pauseTime = x; name = n; } public void run() { while(true) { try { System.out.println(name + ":" + new Date(System.currentTimeMillis())); Thread.sleep(pauseTime); } catch(Exception e) { System.out.println(e); } } } static public void main(String args[]) { Thread t1 = new Thread(new TimePrinter(1000, "Fast Guy")); t1.start(); Thread t2 = new Thread(new TimePrinter(3000, "Slow Guy")); t2.start(); } }

使用时请注意 runnable 当接口时,您不能直接创建所需类别的对象并操作它;必须从 Thread 在内部操作类的一个例子。许多程序员更喜欢它 runnable 接口,因为从 Thread 类别继承会强加类别层次。

synchronized 关键字 到目前为止,我们看到的例子只是以一种非常简单的方式使用线程。只有最小的数据流,不会有两个线程访问同一对象。然而,在大多数有用的程序中,线程之间通常有信息流。试着考虑一个金融应用程序,它有一个 Account 对象,如下例所示:

一家银行的多项活动

public class Account { String holderName; float amount; public Account(String name, float amt) { holderName = name; amount = amt; } public void deposit(float amt) { amount += amt; } public void withdraw(float amt) { amount -= amt; } public float checkBalance() { return amount; } }

一个错误潜伏在这个代码样例中。如果用于单线程应用程序,就不会有问题。但是,在多线程应用程序中,不同的线程可以同时访问同一个线程 Account 例如,联合账户的所有者是不同的 ATM 同时进行访问。在这种情况下,存入和支出可能会以这种方式发生:一个事务被另一个事务覆盖。这将是灾难性的。但是,Java 编程语言为防止这种覆盖提供了一个简单的机制。每个对象在运行过程中都有一个相关的锁。该锁可以通过添加关键字来添加关键字 synchronized 来获得。这样,修改过的 Account 对象(如下所示)不会遭受数据损坏等错误:

同步处理一家银行的多项活动

public class Account { String holderName; float amount; public Account(String name, float amt) { holderName = name; amount = amt; } public synchronized void deposit(float amt) { amount += amt; } public synchronized void withdraw(float amt) { amount -= amt; } public float checkBalance() { return amount; } }

deposit() 和 withdraw() 所有的函数都需要这个锁来操作,所以当一个函数运行时,另一个函数就会被阻塞。请注意,checkBalance() 没有更改,它严格是读函数。因为 checkBalance() 没有同步处理,所以任何其他方法都不会阻止它,也不会阻止任何其他方法,无论这些方法是否同步。

Java 高级多线程支持编程语言

线程组 线程是由个人创建的,但可以归类为线程组,以便于调试和监控。它只能在创建线程的同时与线程组相关联。在使用大量线程的程序中,使用线程组织线程可能非常有帮助。它们可以被视为计算机上的目录和文件结构。

线程间发信 只有当线程需要等待一个条件才能继续执行时 synchronized 关键词是不够的。虽然 synchronized 关键字阻止并发更新一个对象,但它没有实现线程间发信。Object 类为此提供了三个函数:wait()、notify() 和 notifyAll()。以全球气候预测程序为例。这些程序将地球分为许多单元。在每个循环中,每个单元的计算都是隔离的,直到这些值趋于稳定,然后一些数据将在相邻单元之间交换。因此,本质上,在每个循环中,每个线程都必须等待所有线程完成自己的任务才能进入下一个循环。该模型称为屏蔽同步,下例说明该模型:

屏蔽同步

public class BSync { int totalThreads; int currentThreads; public BSync(int x) { totalThreads = x; currentThreads = 0; } public synchronized void waitForAll() { currentThreads++; if(currentThreads < totalThreads) { try { wait(); } catch (Exception e) {} } else { currentThreads = 0; notifyAll(); } } }

调用一个线程 wait() 当线程被有效阻塞时,只有在另一个线程调用同一对象 notify() 或 notifyAll() 到目前为止。因此,在前一个例子中,不同的线程在完成工作后会被调用 waitForAll() 最后一个线程将触发函数 notifyAll() 函数,该函数将释放所有线程。第三个函数 notify() 只通知一个正在等待的线程,这个函数在每次访问只能由一个线程使用的资源时非常有用。然而,不可能预测哪个线程会得到这个通知,因为这取决于 Java 虚拟机 (JVM) 调度算法。

将 CPU 给另一个线程 当线程放弃稀有资源(如数据库连接或网络端口)时,它可以调用 yield() 临时降低函数的优先级,使其他线程能够运行。

守护线程 用户线程和守护线程有两种类型。用户线程是完成有用工作的线程。只提供辅助功能的线程是守护线程。Thread 类提供了 setDaemon() 函数。Java 该程序将运行到所有用户线程终止,然后它将破坏所有保护线程。在 Java 虚拟机 (JVM) 中,即使在 main 之后,如果另一个用户线程仍在运行,程序仍然可以继续运行。

避免不提倡使用的方法 那些不提倡使用的方法是为了支持向后兼容而保留的方法,它们可能会出现在以后的版本中,也可能不会出现。Java 版本中支持多线程 1.1 和版本 1.2 中做了重大修订,stop()、suspend() 和 resume() 不再提倡使用函数。这些函数在 JVM 可能会引入微妙的错误。虽然函数名可能听起来很诱人,但请抵制诱惑,不要使用它们。

调试线程化程序 在线程程序中,死锁、活锁、内存损坏和资源耗尽是一些常见而烦人的情况。

死锁 死锁可能是多线程序中最常见的问题。当一个线程需要一个资源,另一个线程持有该资源的锁时,就会发生死锁。通常很难检测到这种情况。然而,解决方案相当好:在所有线程中按相同的顺序获取所有资源锁。例如,如果有四种资源 —A、B、C 和 D — 而且一个线程可能需要在四个资源中获得任何一个资源的锁,请确保在获得正确的资源时 B 在锁定之前,先得到正确的锁 A 锁,以此类推。如果“线程 希望得到对的 B 和 C 锁,而“线程” 2”获取了 A、C 和 D 锁,这种技术可能会导致阻塞,但它永远不会导致这四个锁的死锁。

活锁 当一个线程忙于接受新任务,永远没有机会完成任何任务时,就会发生生活锁。这个线程最终会超过缓冲区,导致程序崩溃。想象一下,一个秘书需要输入一封信,但她一直忙于接电话,所以这封信永远不会输入。

内存损坏 若使用明智 synchronized 关键词可以完全避免内存错误等愤怒问题。

资源耗尽 有些系统资源有限,如文件描述符。由于每个线程都希望拥有这样的资源,多线程程序可能会耗尽资源。若线程数相当大,或者某一资源的候选线程数远远超过可用资源数,则最好使用资源池。最好的例子是数据库连接池。只要线程需要用数据库连接,它就会从池中取出一个,使用后再返回池中。资源池又称资源库。

调试大量线程 有时候一个程序很难调试,因为有很多线程在运行。在这种情况下,以下类别可能会派上用场:

public class Probe extends Thread { public Probe() {} public void run() { while(true) { Thread[] x = new Thread[100]; Thread.enumerate(x); for(int i=0; i<100; i++) { Thread t = x[i]; if(t == null) break; else System.out.println(t.getName() + "\t" + t.getPriority() + "\t" + t.isAlive() + "\t" + t.isDaemon()); } } } }

限制线程优先级和调度 Java 线程模型涉及可动态更改的线程优先级。从本质上讲,线程的优先级是 1 到 10 之间的数字越大,任务就越紧急。JVM 先调用优先级较高的线程,再调用优先级较低的线程。然而,该标准随机处理具有相同优先级的线程。如何处理这些线程取决于基层操作系统策略。在某些情况下,优先级相同的线程分时运行;在其他情况下,线程将运行到最后。请记住,Java 支持 10 基层操作系统支持的优先级可能要少得多,会造成一些混乱。因此,优先级只能作为一种非常粗略的工具使用。最终的控制可以明智地使用 yield() 完成函数。一般情况下,请不要依靠线程优先级来控制线程状态。

小结 本文说明了在 Java 如何在程序中使用线程。更重要的问题,如是否应该使用线程,在很大程序上取决于手头的应用程序。决定是否在应用程序中使用多线程的一种方法是估计可以并行运行的代码量。并记住以下几点:

多线程的使用不会增加 CPU 的能力。但如果使用的话 JVM 当地线程实现时,不同的线程可以在不同的处理器上同时运行(多 CPU 在机器中),从而使多 CPU 充分利用机器。 如果应用程序是计算密集型的,并且受到影响 CPU 对功能的限制只有很多 CPU 这台机器可以从更多的线程中受益。 当应用程序必须等待缓慢的资源(如网络连接或数据库连接),或者当应用程序是非交互式的时候,多线程通常是有益的。

基于 Internet 多线程软件是必要的;否则,用户会觉得应用程序反应迟钝。例如,当开发需要支持大量客户机器的服务器时,多线程可以使编程更容易。在这种情况下,每个线程可以为不同的客户或客户组服务,从而缩短响应时间。

有些程序员可能在那里 C 在其他语言中使用线程,在这些语言中没有语言支持线程。这些程序员通常对线程失去信心。