并发的基础知识

来源:互联网 发布:js旋转div 编辑:程序博客网 时间:2024/06/06 00:28

什么是线程?

所有重要的操作系统都支持进程的概念 —— 独立运行的程序,在某种程度上相互隔离。

线程有时称为 轻量级进程。与进程一样,它们拥有通过程序运行的独立的并发路径,并且每个线程都有自己的程序计数器,称为堆栈和本地变量。然而,线程存在于进程中,它们与同一进程内的其他线程共享内存、文件句柄以及每进程状态。

今天,几乎每个操作系统都支持线程,允许执行多个可独立调度的线程,以便共存于一个进程中。因为一个进程中的线程是在同一个地址空间中执行的,所以多个线程可以同时访问相同对象,并且它们从同一堆栈中分配对象。虽然这使线程更易于与其他线程共享信息,但也意味着您必须确保线程之间不相互干涉。

正确使用线程时,线程能带来诸多好处,其中包括更好的资源利用、简化开发、高吞吐量、更易响应的用户界面以及能执行异步处理。

Java 语言包括用于协调线程行为的原语,从而可以在不违反设计原型或者不破坏数据结构的前提下安全地访问和修改共享变量。 

 

线程有哪些功能?

在 Java 程序中存在很多理由使用线程,并且不管开发人员知道线程与否,几乎每个 Java 应用程序都使用线程。许多 J2SE 和 J2EE 工具可以创建线程,如 RMI、Servlet、Enterprise JavaBeans 组件和 Swing GUI 工具包。

使用线程的理由包括:

  • 更易响应的用户界面。 事件驱动的 GUI 工具包(如 AWT 或 Swing)使用单独的事件线程来处理 GUI 事件。从事件线程中调用通过 GUI 对象注册的事件监听器。然而,如果事件监听器将执行冗长的任务(如文档拼写检查),那么 UI 将出现冻结,因为事件线程直到冗长任务完毕之后才能处理其他事件。通过在单独线程中执行冗长操作,当执行冗长后台任务时,UI 能继续响应。
  • 使用多处理器。 多处理器(MP)系统变得越来越便宜,并且分布越来越广泛。因为调度的基本单位通常是线程,所以不管有多少处理器可用,一个线程的应用程序一次只能在一个处理器上运行。在设计良好的程序中,通过更好地利用可用的计算机资源,多线程能够提高吞吐量和性能。
  • 简化建模。 有效使用线程能够使程序编写变得更简单,并易于维护。通过合理使用线程,个别类可以避免一些调度的详细、交叉存取操作、异步 IO 和资源等待以及其他复杂问题。相反,它们能专注于域的要求,简化开发并改进可靠性。
  • 异步或后台处理。 服务器应用程序可以同时服务于许多远程客户机。如果应用程序从 socket 中读取数据,并且没有数据可以读取,那么对 read() 的调用将被阻塞,直到有数据可读。在单线程应用程序中,这意味着当某一个线程被阻塞时,不仅处理相应请求要延迟,而且处理所有请求也将延迟。然而,如果每个 socket 都有自己的 IO 线程,那么当一个线程被阻塞时,对其他并发请求行为没有影响。

线程安全

如果将这些类用于多线程环境中,虽然确保这些类的线程安全比较困难,但线程安全却是必需的。java.util.concurrent 规范进程的一个目标就是提供一组线程安全的、高性能的并发构建块,从而使开发人员能够减轻一些编写线程安全类的负担。

线程安全类非常难以明确定义,大多数定义似乎都是完全循环的。快速 Google 搜索会显示下列线程安全代码定义的例子,但这些定义(或者更确切地说是描述)通常没什么帮助:

  • . . . can be called from multiple programming threads withoutunwanted interaction between the threads.
  • . . . may be called by more than on thread at a time withoutrequiring any other action on the caller's part.

通过类似这样的定义,不奇怪我们为什么对线程安全如此迷惑。这些定义几乎就是在说“如果可以从多个线程安全调用类,那么该类就是线程安全的”。这当然是线程安全的解释,但对我们区别线程安全类和不安全类没有什么帮助。我们使用“安全”是为了说明什么?

要成为线程安全的类,首先它必须在单线程环境中正确运行。如果正确实现了类,那么说明它符合规范,对该类的对象的任何顺序的操作(公共字段的读写、公共方法的调用)都不应该使对象处于无效状态;观察将处于无效状态的对象;或违反类的任何变量、前置条件或后置条件。

而且,要成为线程安全的类,在从多个线程访问时,它必须继续正确运行,而不管运行时环境执行那些线程的调度和交叉,且无需对部分调用代码执行任何其他同步。结果是对线程安全对象的操作将用于按固定的整体一致顺序出现所有线程。

如果没有线程之间的某种明确协调,比如锁定,运行时可以随意在需要时在多线程中交叉操作执行。

在 JDK 5.0 之前,确保线程安全的主要机制是 synchronized 原语。访问共享变量(那些可以由多个线程访问的变量)的线程必须使用同步来协调对共享变量的读写访问。java.util.concurrent 包提供了一些备用并发原语,以及一组不需要任何其他同步的线程安全实用程序类。 

 

令人厌烦的并发

即使您的程序从没有明确创建线程,也可能会有许多工具或框架代表您创建了线程,这时要求从这些线程调用的类是线程安全的。这样会对开发人员带来较大的设计和实现负担,因为开发线程安全类比开发非线程安全类有更多要注意的事项,且需要更多的分析。

AWT 和 Swing
这些 GUI 工具包创建了称为时间线程的后台线程,将从该线程调用通过 GUI 组件注册的监听器。因此,实现这些监听器的类必须是线程安全的。

TimerTask
JDK 1.3 中引入的 TimerTask 工具允许稍后执行任务或计划定期执行任务。在 Timer 线程中执行 TimerTask 事件,这意味着作为 TimerTask 执行的任务必须是线程安全的。

Servlet 和 JavaServer Page 技术
Servlet 容器可以创建多个线程,在多个线程中同时调用给定 servlet,从而进行多个请求。因此 servlet 类必须是线程安全的。

RMI
远程方法调用(remote method invocation,RMI)工具允许调用其他 JVM 中运行的操作。实现远程对象最普遍的方法是扩展 UnicastRemoteObject。例示 UnicastRemoteObject 时,它是通过 RMI 调度器注册的,该调度器可能创建一个或多个线程,将在这些线程中执行远程方法。因此,远程类必须是线程安全的。

正如所看到的,即使应用程序没有明确创建线程,也会发生许多可能会从其他线程调用类的情况。幸运的是,java.util.concurrent 中的类可以大大简化编写线程安全类的任务。 

 

例子 —— 非线程安全 servlet

下列 servlet 看起来像无害的留言板 servlet,它保存每个来访者的姓名。然而,该 servlet 不是线程安全的,而这个 servlet 应该是线程安全的。问题在于它使用 HashSet 存储来访者的姓名,HashSet 不是线程安全的类。

当我们说这个 servlet 不是线程安全的时,是说它所造成的破坏不仅仅是丢失留言板输入。在最坏的情况下,留言板数据结构都可能被破坏并且无法恢复。

public class UnsafeGuestbookServlet extends HttpServlet {        private Set visitorSet = new HashSet();    protected void doGet(HttpServletRequest httpServletRequest,              HttpServletResponse httpServletResponse) throws ServletException, IOException {        String visitorName = httpServletRequest.getParameter("NAME");        if (visitorName != null)            visitorSet.add(visitorName);    }}

通过将 visitorSet 的定义更改为下列代码,可以使该类变为线程安全的:

    private Set visitorSet = Collections.synchronizedSet(new HashSet());

如上所示的例子显示线程的内置支持是一把双刃剑 —— 虽然它使构建多线程应用程序变得很容易,但它同时要求开发人员更加注意并发问题,甚至在使用留言板 servlet 这样普通的东西时也是如此。