第1章 简介
第1章 简介
陶邦仁 发表于3年前
第1章 简介
  • 发表于 3年前
  • 阅读 117
  • 收藏 6
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

编写正确的程序很难,而编写正确的并发程序则难上加难。与串行程序相比,并发程序中存在更多容易出错的地方。那么,为什么还要编写并发程序?线程是Java语言中不可或缺的重要功能,它们能使复杂的异步代码变得更简单,从而极大地简化了复杂系统的开发。此外,要想充分发挥多处理器系统的强大计算能力,最简单的方式就是使用线程。随着处理器数量的增长,如何高效地使用并发正变得越来越重要。 ##1.1 并发简史## 操作系统的出现使得计算机每次能运行多个程序,并且不同的程序都在单独的进程中运行:操作系统为各个独立执行的进程分配各种资源,包括内存,文件句柄,以及安全证书等。如果需要的话,在不同的进程之间可以通过一些粗粒度的通信机制来交换数据,包括:套接字,信号处理器,共享内存,信号量以及文件等。

串行编程模型的优势在于其直观性和简单性,因为它模仿了人类的工作方式:每次只做一件事情,做完之后再做另一件。

线程允许在同一个进程中同时存在多个程序控制流。线程会共享进程范围内的资源,例如:内存句柄和文件句柄,但每个线程都有各自的程序计数器,栈以及局部变量等。线程还提供了一种直观的分解模式来充分利用多处理器系统中的硬件并行性,而在同一个程序中的多个线程也可以被同时调度到多个CPU上运行。

线程也被称为轻量级进程。在大多数现代操作系统中,都是以线程为基本的调度单位,而不是进程。如果没有明确的协同机制,那么线程将彼此独立执行。由于同一个进程中的所有线程都将共享进程的内存地址空间,因此这些线程都能访问相同的变量并在同一个堆上分配对象,这就需要实现一种比在进程间共享数据粒度更细的数据共享机制。如果没有明确的同步机制来协同对共享数据的访问,那么当一个线程正在使用某个变量时,另一个线程可能同时访问这个变量,这将造成不可预测的结果。 ##1.2 线程的优势## 如果使用得当,线程可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。线程能够将大部分的异步工作流转换成串行工作流,因此能更好地模拟人类的工作方式和交互方式。此外,线程还可以降低代码的复杂度,使代码更容易编写,阅读和维护。 ###1.2.1 发挥多处理器的强大能力### 由于基本的调度单位是线程,因此如果在程序中只有一个线程,那么最多同时只能在一个处理器上运行。在双处理器系统上,单线程的程序只能使用一半的CPU资源,而在拥有100个处理器的系统上,将有99%的资源无法使用。另一方面,多线程程序可以同时在多个处理器上执行。如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率。

使用多个线程还有助于在单处理器系统上获得更高的吞吐率。如果程序是单线程的,那么当程序等待某个同步I/O操作完成时,处理器将处于空闲状态。而在多线程程序中,如果一个线程在等待I/O操作完成,另一个线程可以继续运行,使程序能够在I/O阻塞期间继续运行。 ###1.2.2 建模的简单性### 如果在程序中只包含一种类型的任务,那么比包含多种不同类型任务的程序要更易于编写,错误更少,也更容易测试。如果为模型中每种类型的任务都分配一个专门的线程,那么可以形成一种串行执行的假象,并将程序的执行逻辑与调度机制的细节,交替执行的操作,异步I/O以及资源等待等问题分离开来。通过使用线程,可以将复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。 ###1.2.3 异步事件的简化处理### 服务器应用程序在接受来自多个远程客户端的套接字连接请求时,如果为每个连接都分配其各自的线程并且使用同步I/O,那么就会降低这类程序的开发难度。

如果某个应用程序对套接字执行读操作而此时还没有数据到来,那么这个读操作将一直阻塞,直到有数据到达。在单线程应用程序中,这不仅意味着在处理请求的过程中将停顿,而且还意味着在这个线程被阻塞期间,对所有请求的处理都将被停顿。为了避免这个问题,单线程服务器应用程序必须使用非阻塞I/O,这种I/O的复杂性要远远高于同步I/O,并且很容易出错。然而,如果每个请求都拥有自己的处理线程,那么在处理某个请求时发生的阻塞将不会影响其他请求的处理。

因此,操作系统提供了一些高效的方法来实现多路I/O,例如:Unix的select和poll等系统调用,要调用这些方法,Java类库需要获得一组实现非阻塞I/O的包(java.nio)。非阻塞I/O有其自身的优势,但如果操作系统能更好地支持线程,那么需要使用非阻塞I/O的情况将变得更少。 ##1.3 线程带来的风险## ###1.3.1 安全性问题### 线程安全性可能是非常复杂的,在没有充足同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果。

由于多个线程要共享相同的内存地址空间,并且是并发运行,因此它们可能会访问或修改其他线程正在使用的变量。当然,这是一种极大的便利,因为这种方式比其他线程间通信机制更容易实现数据共享。但同样也带来了巨大的风险:线程会由于无法预料的数据变化而发生错误。当多个线程同时访问和修改相同的变量时,将会在串行编程模式中引入非串行因素,而这种非串行性是很难分析的。要使多线程程序的行为可以预测,必须对共享变量的访问操作进行协同,这样才不会在线程之间发生彼此干扰。

如果没有同步,那么无论是编译器,硬件还是运行时,都可以随意安排操作的执行时间和顺序,例如对寄存器或者处理器中的变量进行缓存,而这些被缓存的变量对于其他线程来说是暂时(甚至永久)不可见的。虽然这些技术有助于实现更优的性能,并且通常也是值得采用的方法,但它们也为开发人员带来了负担,因为开发人员必须找出这些数据在哪些位置被多个线程共享,只有这样才能使这些优化措施不破坏线程安全性。 ###1.3.2 活跃性问题### 在开发并发代码时,一定要注意线程安全性是不可破坏的。安全性不仅对于多线程程序很重要,对于单线程程序同样重要。此外,多线程还会导致一些在单线程中不会出现的问题,例如:活跃性问题。

安全性的含义是“永远不会发生糟糕的事情”,而活跃性则关注另一个目标,即“某件正确的事情最终会发生”。当某个操作无法执行下去时,就会发生活跃性问题。在串行程序中,活跃性问题的形式之一就是无意中造成的死循环,从而使循环之后的代码无法得到执行。多线程将带来其他一些活跃性问题,与大多数并发性错误一样,导致活跃性问题的错误同样是难以分析的,因为它们依赖于不同线程的事件发生时序,因此在开发或者测试中并不总是能够重现。 ###1.3.3 性能问题### 活跃性意味着某件正确的事情最终会发生,但却不够好,因为我们通常希望正确的事情尽快发生。性能问题包括多个方面,例如服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高,或者可伸缩性较低等。与安全性和活跃性一样,在多线程程序中不仅存在与单线程程序相同的性能问题,而且还存在由于使用线程而引入的其他性能问题。

在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加共享内存总线的同步流量。 ##1.4 线程无处不在## 每个Java应用程序都会使用线程。当JVM启动时,它将为JVM的内部任务(例如:垃圾收集,终结操作等)创建后台线程,并创建一个主线程来运行main()方法。

当某个框架在应用程序中引入并发性时,通常不可能将并发性仅局限于框架代码,因为框架本身会回调(Callback)应用程序的代码,而这些代码将访问应用程序的状态。同样,对线程安全性的需求也不能局限于被调用的代码,而是要延伸到需要访问这些代码所访问的程序状态的所有代码路径。因此,对线程安全性的需求将在程序中蔓延开来。

Timer:Timer类的作用是使任务在稍后的时刻运行,或者运行一次,或者周期性地运行。引入Timer可能会使串行程序变得复杂,因为TimerTask将在Timer管理的线程中执行,而不是由应用程序来管理。如果某个TimerTask访问了应用程序中其他线程访问的数据,那么不仅TimerTask需要以线程安全的方式来访问数据,其他类也必须采用线程安全的方式来访问该数据。通常,要实现这个目标,最简单的方式是确保TimerTask访问的对象本身是线程安全的,从而就能把线程安全性封装在共享对象内部。

Servlet和JSP:Servlet框架用于部署网页应用程序以及分发来自HTTP客户端的请求。到达服务器的请求可能会通过一个过滤器链被分发到正确的Servlet或JSP。每个Servlet都表示一个程序逻辑组件,在高吞吐率的网站中,多个客户端可能同时请求同一个Servlet的服务。在Servlet规范中,Servlet同样需要满足被多个线程同时调用,换句话说,Servlet需要是线程安全的。

即使你可以确保每次只有一个线程调用某个Servlet,但在构建网页应用程序时仍然必须注意线程安全性。Servlet通常会访问与其他Servlet共享的信息,例如应用程序中的对象(这些对象保存在ServletContext中)或者会话中的对象(这些对象保存在每个客户端的HttpSession中)。当一个Servlet访问在多个Servlet或者请求中共享的对象时,必须正确地协同对这些对象的访问,因为多个请求可能在不同的线程中同时访问这些对象。Servlet和JSP,以及在ServletContext和HttpSession等容器中保存的Servlet过滤器和对象等,都必须是线安全的。

远程访问调用(RMI):RMI使代码能够调用在其他JVM中运行的对象。当通过RMI调用某个远程方法时,传递给方法的参数必须打包(也称为列集)到一个字节流中,通过网络传输给远程JVM,然后由远程JVM拆包(或者称为散集)并传递给远程方法。

当RMI代码调用远程对象时,这个调用在哪个线程中执行?你并不知道,但肯定不会在你创建的线程中,而是将在一个由RMI管理的线程中调用对象。RMI会创建多少个线程?同一个远程对象上的同一个远程方法会不会在多个RMI线程中被同时调用?

远程对象必须注意两个线程安全性问题:正确地协同在多个对象中共享的状态,以及对远程对象本身状态的访问(由于同一个对象可能会在多个线程中被同时访问)。与Servlet相同,RMI对象应该做好被多个线程同时调用的准备,并且必须确保它们自身的线程安全性。

共有 人打赏支持
粉丝 1467
博文 388
码字总数 1483822
×
陶邦仁
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: