》》并发简史
@@ 操作系统的出现使得计算机每次能运行多个程序,并且不同的程序都在单独的进程中运行:
操作系统为各个独立执行的进程分配各种资源,包括内存、文件句柄以及安全证书等。
如果需要的话,在不同的进程之间可以通过一些粗粒度的通信机制来交换数据,包括:
套接字、信号处理器、共享内存、信号量以及文件等。
@@ 在计算机中加入操作系统来实现多个程序的同时执行,主要是基于以下原因:
------ 资源利用率
------ 公平性。不同的用户和程序对于计算机上的资源有着同等的使用权。一种高效的运行
方式是通过粗粒度的时间分片(Time Slicing)使这些用户和程序能共享计算机资源,而
不是由一个程序从头运行到尾,然后再启动下一下程序。
------ 便利性。通常来说,在计算多个任务时,应该编写多个程序,每个程序执行一个任务
并在必要时互相通信,这比只编写一个程序来计算所有任务更容易实现。
@@ 促使进程出现的因素(资源利用率、公平性、便利性等)同样也促使着线程的出现。
@@ 线程允许在同一个进程中同时存在多个控制流。
@@ 线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器
( Program Counter)、栈以及局部变量等。
@@ 线程还提供了一种直观的分解模式来充分利用多处理器系统中的硬件并行性,而在同一程序
中的多个线程也可以被同时调度到多个 CPU 上运行。
@@ 线程也被称为轻量级进程。在大多数现代操作系统中,都是以线程为基本的调度单位,而不是
进程。
@@ 如果没有明确的协同机制,那么线程将彼此独立执行。
@@ 由于同一个进程中的所有线程都将共享进程的内存地址空间,因此这些线程都能访问相同
的变量并在同一堆上分配对象,这就需要实现一种比在进程间共享数据粒度更细的数据共享
机制。
@@ 如果没有明确的同步机制来协同对共享数据的访问,那么当一个线程正在使用某个变量时,
另一个线程可能同时访问这个变量,这将造成不可预测的结果。
》》线程的优势
@@ 线程可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。
@@ 线程能够将大部分的异步工作流转换成串行工作流,因此能更好地模拟人类的工作方式和
交互方式。
@@ 线程可以降低代码的复杂度,使代码更容易编写、阅读和维护。
@@ 在 GUI (图形用户界面)应用程序中,线程可以提高用户界面的响应灵敏度,而在服务器
应用程序中,可以提升资源利用率以及系统吞吐量。
@@ 线程可以简化 JVM 的实现,垃圾收集器通常在一个或多个专门的线程中运行。
## 发挥多处理器的强大能力
@@ 如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐量。
## 建模的简单性
@@ 如果需要完成多种类型的任务,那么需要管理不同任务之间的优先级和执行时间,并在
任务之间进行切换,这将带来额外的开销。
@@ 通过使用线程,可以将复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,
每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。
补充:
---------- 可以通过一些现有的框架来实现上述的目标,例如 Servlet 和 RMI (远程方法调用)
---------- 框架负责解决一些细节问题,例如请求管理 、 线程创建 、 负载平衡,并在正确
的时刻将请求分发给正确的应用程序组件。
---------- 当调用 Servlet 的 service 方法来响应 Web 请求时,可以以同步方式来处理这个
请求,就好像它是一个单线程程序。这种方式可以简化组件的开发,并缩短掌握
这种框架的学习时间。
## 异步事件的简化处理
@@ 服务器应用程序在接受来自多个远程客户端的套接字连接请求时,如果为每个连接都
分配其各自的线程并且使用同步 I / O , 那么就会降低这类程序的开发难度。
@@ 单线程服务器程序必须使用非阻塞 I / O ,这种 I / O 的复杂性要远远高于同步 I / O ,
并且很容易出错。然而,如果每个请求都拥有自己的处理线程,那么在处理某个请求
时发生的阻塞将不会影响其他请求的处理。
补充:Java类库需要获得一组实现非阻塞 I / O 的包 (java.nio)
## 响应更灵敏的用户界面(GUI)
@@ 传统的 GUI 应用程序通常都是单线程的,从而在代码的各个位置都需要调用 poll
方法来获取输入事件(这种方式将给代码带来极大的混乱),或者通过一个
” 主事件循环 “ 来间接的执行应用程序的所有代码。
如果在主事件循环中调用的代码需要很长时间才能执行完成,那么用户界面
就会” 冻结 “ ,直到代码执行完成。这是因为只有当执行控制权返回到主事件循环后,
才能执行后续的用户界面事件。
@@ 在现代的 GUI 框架中,例如 AWT 和 Swing 等工具,都采用一个事件分发线程
(EDT)来替代主事件循环。
当某个用户界面事件发生时(例如按下一个按钮),在事件线程中将调用应用
程序的事件处理器。
由于大多数 GUI 线程都是单线程子系统,因此到目前为止仍然存在主事件循环,
但它现在处于 GUI 工具的控制下并在其自己的线程中运行,而不是在应用程序的
控制下。
@@ 如果事件线程中执行的任务都是短暂的,那么界面的响应灵敏度就较高,因为事件
线程能够很快地处理用户的动作。
@@ 如果事件线程中的任务需要很长的执行时间,例如对一个大型文件进行拼写检查,
或者从网上获取一个资源,那界面的响应灵敏度就会降低。
更糟糕的是,不仅界面失去响应,而且即使在界面上包含了 ” 取消 “ 按钮,也
无法取消这个长时间执行的任务,因为事件线程只有在执行完该任务后才能响应
” 取消 “按钮的点击事件。
@@ 如果将长时间运行的任务放在一个单独的线程中运行,那么事件线程就能及时地
处理界面事件,从而使用户界面具有更高的灵敏度。
》》线程带来的风险
## 安全性问题
@@ 线程安全可能是非常复杂的,在没有充足同步的情况下,多个线程中的操作
执行顺序是不可预测的,甚至会产生奇怪的结果。
@@ 代码示例:
非线程安全的数值序列生成器
public class UnsafeSequence {
private int value ;
/** 返回一个唯一的数值 **/
public int getValue( ) {
return value++ ;
}
}
说明:上述类中是一种常见的并发安全问题,称为竞态条件。在多线程环境下,
getValue( ) 是否会返回唯一的值,要取决于运行时对线程中操作的交替执行方式。
@@ 将上面的代码改为线程安全的,代码如下:(使用同步机制来协同多线程的访问)
public class Sequence {
private int value ;
public synchronized int getValue( ) {
return value++ ;
}
}
## 活跃性问题
@@ 在开发并发代码时,一定要注意线程安全是不可破坏的。安全性不仅对多线程很重要,
对于单线程程序同样重要。
@@ 线程还会导致一些在单线程程序中不会出现的问题,例如活跃性问题。
@@ 安全性的目标:永远不发生糟糕的事情。
活跃性的目标:某件正确的事情最终会发生。
(当某个操作无法继续执行下去时,就会发生活跃性问题。)
## 性能问题
@@ 与活跃性问题密切相关的是性能问题。活跃性意味着某件正确的事情最终会发生,但却
不够好,因为我们通常希望正确地事情尽快发生。
@@ 性能问题包括多个方面,例如服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高,
或者可伸缩性较低等。
@@ 在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁
地出现上下文切换操作(Context Switch),这种操作将带来极大的开销:保存和恢复执行
上下文,丢失局部性,并且 CPU 时间将更多地花在线程调度而不是线程运行上。
@@ 当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存
缓存区中的数据无效,以及增加共享内存总线的同步流量。
》》线程无处不在
@@ 开发线程安全的类比开发非线程安全的类更加谨慎和细致。
@@ 每个 Java 应用程序都会使用线程。
------ 当 JVM 启动时,它将为 JVM 的内部任务(例如,垃圾收集 、 终结操作等)创建
后台线程,并创建一个主线程来运行 main 方法。
------ AWT 和 Swing 的用户界面框架将创建线程来管理用户界面事件。
------ Timer 将创建线程来执行延迟任务。
------ 一些组件框架,例如 Servlet 和 RMI ,都会创建线程池并调用这些线程中的方法。
@@ 几乎所有的 Java 应用程序都是多线程的,因此在使用框架时仍然需要对应用程序状态
的访问进行协同。
@@ 框架通过在框架线程中调用应用程序代码将并发性引入到程序中。在代码中将不可避免
地访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的。
@@ Timer
-------- Timer 类的作用是使任务在稍后的时刻执行,或者运行一次,或者周期性地运行。
------- 引入 Timer 可能会使串行程序变得复杂,因为 TimerTask 将在 Timer 管理的线程
中执行,而不是由应用程序来管理。
------- 如果某个 TimerTask 访问了应用程序中其他线程访问的数据,那么不仅 TimerTask
需要以线程安全的方式来访问数据,其他类也必须采用线程安全的方式来访问该数据。
通常,要实现上述目标,最简单的方式是确保 TimerTask 访问的对象本身是线程
安全的,从而就能把线程安全性封装在共享对象内部。
@@ Servlet 和 JavaServer Page (JSP)
------- Servlet 框架用于部署网页应用程序以及分发来自 HTTP 客户端的请求。到达服务器
的请求可能会通过一个过滤器链被分发到正确的 Servlet 或 JSP 。
-------- 每个 Servlet 都表示一个程序逻辑组件,在高吞吐率的网站中,多个客户端可能同时
请求同一个 Servlet 服务。在 Servlet 规范中, Servlet 同样需要满足被多个线程同时
调用,换句话说, Servlet 需要是线程安全地。
-------- Servlet 通常会访问与其他 Servlet 共享的信息,例如应用程序中的对象(这些对象
保存在 ServletContext 中)或者会话中的对象(这些对象保存在每个客户端的
HttpSession 中)。
-------- 当一个 Servlet 访问在多个 Servlet 或者请求中共享的对象时,必须正确地协同对
这些对象的访问,因为多个请求可能在不同的线程中同时访问这些对象。
--------- Servlet 和 JSP ,以及在 ServletContext 和 HttpSession 等容器中保存的 Servlet
过滤器和对象等,都必须是线程安全的。
@@ 远程方法调用(RMI)
--------- RMI 使代码能够调用在其他 JVM 中运行的对象。当通过 RMI 调用某个远程方法时,
传递给方法的参数必须被打包(也称为列集)到一个字节流中,通过网络传输给远程
JVM ,然后由远程 JVM 拆包(也称为散集)并传递给方法。
--------- 当 RMI 代码调用远程对象时,这个调用将在哪个线程中执行?
将在一个由 RMI 管理的线程中调用对象。
---------- 远程对象必须注意两个线程安全性问题:
%% 正确地协同在多个对象中共享的状态,以及对远程对象本身状态的访问(由
于同一个对象可能会在多个线程中被同时访问)。
%% 与 Servlet 相同, RMI 对象应该做好被多个线程同时调用的准备,并且必须
确保自身的线程安全性。
@@ Swing 和 AWT
---------- GUI 应用程序的一个固有属性是异步性。用户可以在任意时刻选择一个菜单项
或者按下一个按钮,应用程序就会及时响应,即使应用程序当时正在执行其他的任务。
---------- Swing 和 AWT 创建了一个单独的线程来处理用户触发的事件,并对呈现给用户
的图形界面进行更新。
----------- Swing 的一些组件并不是线程安全的,例如 JTable 。
但是,Swing 程序通过将所有对 GUI 组件的访问局限在事件线程中以实现线程安全性。
如果某个应用程序希望在事件线程之外控制 GUI ,那么必须将控制 GUI 的代码放在
事件线程中运行。
----------- 当用户触发某个 UI 动作时,在事件线程中就会有一个事件处理器被调用以执行用户
请求的操作。
如果事件处理器需要访问由其他线程同时访问的应用程序状态(例如编辑某个文档),
那么这个事件处理器,以及访问这个状态的所有其他代码,都必须采用一个线程安全
的方式来访问该状态。