所有重要的操作系统都支持进程的概念 —— 独立运行的程序,在某种程度上相互隔离。
线程有时称为 轻量级进程。与进程一样,它们拥有通过程序运行的独立的并发路径,
并且每个线程都有自己的程序计数器,称为堆栈和本地变量。然而,线程存在于进程中,
它们与同一进程内的其他线程共享 内存 、文件句柄以及每进程状态。
在 Java 程序中存在很多理由使用线程,并且不管开发人员知道线程与否,几乎每个
Java 应用程序都使用线程。许多 J2SE 和 J2EE 工具可以创建线程,如 RMI、Servlet、Enterprise JavaBeans
组件和 Swing GUI 工具包。
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());
JDK 1.2 中引入的 Collection
框架
是一种表示对象集合的高度灵活的框架,
它使用
基本接口 List
、Set
和 Map
。通过 JDK 提供每个集合的多次实现
(
HashMap
、
Hashtable
、TreeMap
、WeakHashMap
、HashSet
、TreeSet
、
Vector
、ArrayList
、
LinkedList
等等)。其中一些集合已经是线程安
全的(
Hashtable
和Vector
),通过同步的封装工厂
(Collections.synchronizedMap()
、synchronizedList()
和 synchronizedSet()
),其余的集合均可表现为线程安全的。
线程创建 |
线程最普遍的一个应用程序是创建一个或多个线程,以执行特定类型的任务。
Timer
类创建线
程来执行 TimerTask
对象,Swing 创建线程来处理 UI 事件。在这两种情况中,
在单独线程
中执行的任务都假定是短期的,这些线程是为了处理大量短期任务而存在的。
在其中每种情况中,这些线程一般都有非常简单的结构:
while (true) {
if (no tasks)
wait for a task;
execute the task;
}
通过例示从 Thread
获得的对象并调用 Thread.start()
方法来创建线程。
可以用两种方法创建线程:通过扩展 Thread
和覆盖 run()
方法,或者通过
实现 Runnable
接口和使用 Thread(Runnable)
构造函数:
class WorkerThread extends Thread {
public void run() { /* do work */ }
}
Thread t = new WorkerThread();
t.start();
或者:
Thread t = new Thread(new Runnable() {
public void run() { /* do work */ }
}
t.start();
如何不对任务进行管理 |
大多数 服务 器应用程序(如 Web 服务器、POP 服务器、数据库服务器或文件
服务器)代表远程客户机处理请求,这些客户机通常使用 socket 连接到服务器。
对于每个请求,通常要进行少量处理(获得该文件的代码块,并将其发送回
socket),但是可能会有大量(且不受限制)的客户机请求服务。
用于构建服务器应用程序的简单化模型会为每个请求创建新的线程。下列代码段
实现简单的 Web 服务器,它接受端口 80 的 socket 连接,并创建新的线程来
处理请求。不幸的是,该代码不是实现 Web 服务器的好方法,因为在重负载条
件下它将失败,停止整台服务器.
class UnreliableWebServer {
public static void main(String[] args) {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable r = new Runnable() {
public void run() {
handleRequest(connection);
}
};
// Don't do this!
new Thread(r).start();
}
}
}
当服务器被请求吞没时,UnreliableWebServer
类不能很好地处理这种情况。
每次有请求时,就会创建新的类。根据操作系统和可用内存,可以创建的线程数
是有限的。不幸的是,您通常不知道限制是多少 —— 只有当应用程序因为
OutOfMemoryError
而崩溃时才发现。
使用线程池解决问题 |
为任务创建新的线程并不一定不好,但是如果创建任务的频率高,而平均任务持
续时间低,我们可以看到每项任务创建一个新的线程将产生性能(如果负载不可
预知,还有稳定性)问题。 如果不是每项任务创建一个新的线程,则服务器应
用程序必须采取一些方法来限制一次可以处理的请求数。这意味着每次需要启动
新的任务时,它不能仅调用下列代码。
new Thread(runnable).start()
管理一大组小任务的标准机制是组合 工作 队列和线程池。工作队列就是要处理的
任务的队列,前面描述的 Queue
类完全适合。线程池是线程的集合,每个线程
都提取公用工作队列。当一个工作线程完成任务处理后,它会返回队列,查看是
否有其他任务需要处理。如果有,它会转移到下一个任务,并开始处理。
Executor 框架 |
java.util.concurrent
包中包含灵活的线程池实现,但是更重要的是,
它包含用于管理实现 Runnable
的任务的执行的整个框架。该框架称为
Executor 框架。
Executor
接口相当简单。它描述将运行Runnable
的对象:
public interface Executor {
void execute(Runnable command);
}
任务运行于哪个线程不是由该接口指定的,这取决于使用的 Executor
的实现。
它可以运行于后台线程,如 Swing 事件线程,或者运行于线程池,或者调用
线程,或者新的线程,它甚至可以运行于其他 JVM!通过同步的 Executor
接口提交任务,从任务执行策略中删除任务提交。Executor
接口独自关注任
务提交 —— 这是 Executor
实现的选择,确定执行策略。这使在部署时调整
执行策略(队列限制、池大小、优先级排列等等)更加容易,更改的代码最少。
Executor |
java.util.concurrent
包包含多个Executor
实现,每个实现都实现不
同的执行策略。什么是执行策略?执行策略定义何时在哪个线程中运行任务,
执行任务可能消耗的资源级别(线程、内存等等),以及如果执行程序超载
该怎么办。 执行程序通常通过工厂方法例示,而不是通过构造函数。Executors
类包含用于构造许多不同类型的 Executor
实现的静态工厂方法:
-
Executors.newCachedThreadPool()
创建不限制大小线程池,但是当以- 前创建的线程可以使用时将重新使用那些线程。如果没有现有线程可用,将创建新的线
- 程并将其添加到池中。使用不到 60 秒的线程将终止并从缓存中删除。
Executors.newFixedThreadPool(int n)
创建线程池,其重新使用在不- 受限制的队列之外运行的固定线程组。在关闭前,所有线程都会因为执行过程中的失败
- 而终止,如果需要执行后续任务,将会有新的线程来代替这些线程。
Executors.newSingleThreadExecutor()
创建 Executor,其使用在不受- 限制的队列之外运行的单一工作线程,与 Swing 事件线程非常相似。保证顺序执行任务,
- 在任何给定时间,不会有多个任务处于活动状态。
class ReliableWebServer { Executor pool = Executors.newFixedThreadPool(7); public static void main(String[] args) { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable r = new Runnable() { public void run() { handleRequest(connection); } }; pool.execute(r); } } }
定制 ThreadPoolExecutor
Executors
中的newFixedThreadPool
和newCachedThreadPool
工厂方法返回的
Executor
是类ThreadPoolExecutor
的实例,是高度可定制的。
通过使用包含 ThreadFactory
变量的工厂方法或构造函数的版本,可以定义池线程的创建。
ThreadFactory
是工厂对象,其构造执行程序要使用的新线程。使用定制的线程工厂,创建
的线程可以包含有用的线程名称,并且这些线程是守护线程,属于特定线程组或具有特定优先级。
下面是线程工厂的例子,它创建守护线程,而不是创建用户线程:
public class DaemonThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); return thread; } }
java.util.concurrent
中其他类别的有用的类也是同步工具。这组类相互协作,控制一个或多个线程的执
行流。
Semaphore
、CyclicBarrier
、CountdownLatch
和Exchanger
类都是同步工具的例子。
每个类都有线程可以调用的方法,方法是否被阻塞取决于正在使用的特定同步
工具的状态和规则。