所有重要的操作系统都支持进程的概念 —— 独立运行的程序,在某种程度上相互隔离。
线程有时称为 轻量级进程。与进程一样,它们拥有通过程序运行的独立的并发路径,
并且每个线程都有自己的程序计数器,称为堆栈和本地变量。然而,线程存在于进程中,
它们与同一进程内的其他线程共享内存、文件句柄以及每进程状态。
在 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类都是同步工具的例子。
每个类都有线程可以调用的方法,方法是否被阻塞取决于正在使用的特定同步
工具的状态和规则。
本文探讨了Java中线程安全的重要性,特别是在GUI组件、Servlet、RMI等场景下。文章还介绍了如何创建线程安全的类,并通过使用java.util.concurrent包中的工具简化这一过程。此外,还讨论了线程池的实现及其对服务器应用程序性能的影响。
170万+

被折叠的 条评论
为什么被折叠?



