《Java并发编程》第一章 — 简介 — 读书笔记

1.1 并发简史

    资源利用率:在某些状况下,程序必须等待某个外部操做执行完成,例如输入操做或输出操做等,而在等待时程序没法执行其余任何工做。所以,若是在等待的同时能够运行另外一个程序,那么无疑将提升资源的利用率。
    公平性:不一样的用户和程序对于计算机上的资源有着同等的使用权。一种高效的运行方式是经过粗颗粒度的时间分片(Time Slicing)使这些用户的程序能共享计算机资源,而不是由一个程序从头运行到尾,而后再启用下一个程序。java

    便利性:一般来讲,在计算多个任务时,应该编写多个程序,每一个程序执行一个任务并在必要时互相通讯,这比只编写一个程序来计算全部任务更容易实现。编程

1.2 线程的优点

    若是使用得当,线程能够有效地下降程序的开发和维护成本,同时提高复杂应用程序的性能。线程可以将大部分的异步工做流转换成串行工做流,所以能更好地模拟人类的工做方式和交互方式。此外,线程还能够下降代码的复杂度,使代码更容易编写、阅读和维护。缓存

    在GUI(Graphic User Interface,用户图形界面)应用程序中,线程能够提升用户界面的响应灵敏度,而在服务器应用程序中,能够提高资源利用率以及系统吞吐率。线程还能够简化JVM的实现,垃圾收集器一般在一个或多个专门的线程中运行。在许多重要的Java应用程序中,都在必定程度上用到了线程。安全

1.2.1 发挥多处理器的强大能力

    过去,多处理器系统的很是昂贵和稀少的。但如今,多处理器系统日益普及,而且价格也不断地下降,即便在低端服务器和中端桌面系统,一般也会采用多个处理器。服务器

    因为基本的调度单位是线程,所以若是在系统中只有一个线程,那么最多同时只能在一个处理器上运行。在双处理器系统上,单线程的程序只能使用一半的CPU次元,而在拥有100个处理器的系统上,将有99%的资源没法使用。另外一方面,多线程程序能够同时在多个处理器上执行。若是设计正确,多线程程序能够经过提升处理器资源的利用率来提高系统的吞吐率。多线程

    使用多个线程还有助于在单处理器系统上得到更高的吞吐率。若是程序是单线程的,那么当程序等待某个同步I/O操做完成时,处理器将处于空闲状态。而在多线程程序中,若是一个线程在等待I/O操做完成,另外一个线程能够继续运行,使程序可以在I/O阻塞期间继续运行。并发

1.2.2 建模的简单性

    一般,当只须要执行一种类型的任务时,在时间管理方面比执行多种类型的任务要简单。当只有一种类型的任务须要完成时,只须要埋头工做,知道完成全部任务,你不须要花任何经理来琢磨下一步该作什么。框架

    例如Servlet和RMI(Remote Method Invocation,远程方法调用)。框架负责解决一些细节问题,例如请求管理、线程建立、负载平衡,并在正确的时刻将请求分发给正确的应用程序组件。编写Servlet的开发人员不须要了解有多少请求在同一时刻被处理,也不须要了解套接字的输入流或者输出流是否被阻塞。当调用Servlet的servlet方法来响应Web请求时,能够以同步方式来处理这个请求,就好像它是一个单线程程序。这种方式能够简化组件的开发,并缩短掌握这种框架的学习时间。异步

1.2.3 异步事件的简化处理

    服务器应用程序接受来自多个远程客户端的套接字链接请求时,若是为每一个连接都分配其各自的线程并使用同步I/O,那么就会下降这类程序的开发难度。工具

    若是某个应用程序对套接字执行读取操做而此时尚未数据到来,那么这个读取将一直阻塞,直到有数据到达。在单线程程序中,这不只意味着在处理请求的过程当中将停顿,并且还意味着在这个线程被阻塞期间,对全部请求的处理都将停顿。为了不这个问题,单线程服务器应用程序必须使用非阻塞I/O,这种I/O的复杂性要远远高于同步I/O,而且很容易出错。然而,若是每一个请求都拥有本身的处理线程,那么在处理某个请求时发生的阻塞将不会影响其余请求的处理。

1.2.4 响应更灵敏的用户界面

    在现代GUI应用程序中,例如AWT和Swing等工具,都采用一个事件分发线程(Event Dispatch Thread, EDT)来代替主事件循环。当某个用户界面事件发生时,在事件线程中将调用应用程序的事件处理器。因为大多数GUI框架都是单线程子系统,所以到目前为止仍然在主事件循环,但它如今处理GUI工具的控制下并在其本身的线程中运行,而不是在应用程序的控制下。

1.3 线程带来的风险

    Java对线程的支持实际上是一把双刃剑。虽然Java提供了相应的语言和库,以及一种明确的跨平台内存模型,这些工具简化了并发应用程序的开发,但同时也提升了对开发人员的技术要求,由于在更多的程序中会使用线程。

1.3.1 安全性问题

    线程安全性多是很是复杂的,在没有充足同步的状况下,多个线程中操做执行顺序是不可预测的,甚至会产生奇怪的结果。

// 线程不安全
public class UnsafeSequence {
	private int value;
	
	// 返回一个惟一的数值
	public int getNext() {
		return value++;
	}
}

    在上面的代码中,UnsafeSequence类中将产生一个整数值序列,该序列中的每一个值都是惟一的。在这个类中简要地说明了多个线程之间的交替操做将如何致使不可预料的结果。在单线程环境中,这个类能正确的工做,但在多线程环境中则不能。

    UnsafeSequence的问题在于,若是执行的时机不对,那么两个线程在调用getNext时会获得相同的值。虽然递增运算看上去是单个操做,但事实上它包含三个独立的操做:读取value,将value加1,并将计算结果写入value。因为运行时可能将多个线程之间的操做交替执行,所以这两个线程可能同时执行读取操做,从而使他们获得相同的值,并都将这个值加1。结果就是,在不一样线程的调用中返回了相同的数值。

    UnsafeSequence类中说明的是一种常见的并发安全问题,成为竞态条件(Race Condition)。在多线程环境下,getValue是否会返回惟一的值,要取决于运行时对线程中操做的交替执行方式,这并非咱们但愿看到的。

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

    经过将getNext修改成一个同步方法,能够修改UnsafeSequence中的错误:

// 线程安全
public class UnsafeSequence {
	private int value;
	
	// 返回一个惟一的数值
	public synchronized int getNext() {
		return value++;
	}
}

    若是没有同步,那么不管是编译器、硬件仍是运行时,均可以随意安排操做的执行时间和顺序。虽然这些技术有助于实现更优秀的性能,而且一般也是值得采用的方法,但它们也为开发人员带来了负担,由于开发人员必须找出这些数据在哪些位置被多个线程共享,只有这样才能使这些优化措施不破坏线程安全性。

1.3.2 活跃性问题

    在开发代码时,必定要注意线程安全性是不可破坏的。安全性不只对于多线程程序很重要,对于单线程程序一样重要。此外,线程还会致使一些在单线程程序中不会出现的问题,例如活跃性问题。

    安全性的含义是“永远不发生糟糕的事情”,而活跃性则关注与另外一个目标,即“某件正确的事情最终会发生”。当某个操做没法继续执行下去时,就会发生活跃性问题。在串行程序中,活跃性问题的形式之一就是无心中形成无限循环,从而使循环以后的代码没法获得执行。线程将带来一些其余的活跃性问题。包括死锁、饥饿、以及活锁。与大多数并发性错误同样,致使活跃性问题的错误一样是难以分析的,由于它们依赖于不一样 线程的事件发生时序,所以在开发或者测试中并不老是可以重现。

1.3.3 性能问题

    与活跃性问题密切相关的是性能问题。活跃性意味着某件正确的事情最终会发生,但却不够好,由于咱们一般但愿正确的事情尽快发生。性能问题包括多个方面,例如服务时间过长,响应不灵敏,吞吐率太低,资源消耗太高,或者可伸缩性较低等。与安全性和活跃性同样,在多线程程序中不只存在于单线程程序相同的性能问题,并且还存在因为使用线程而引入的其余性能问题。

    在设计良好的并发应用程序中,线程能提高程序的性能,但不管如何,线程总会带来某种程度的运行时开销。在多线程程序中,当线程调度器挂起活跃线程并转而运行另一个线程时,就会频繁的出现上下文切换操做(Context Switch),这种操做将带来极大的开销:保存和恢复执行上下文,丢失局部性,而且CPU时间将更多地花在线程调度而不是运行上。当线程共享数据时,必须使用同步机制,而这些机制每每会抑制某些编译器优化,使内存缓存区中的数据无效,以及增长共享内存的总线的同步流量。全部这些因素都将带来额外的性能开销。

1.4 线程无处不在

    每一个Java应用程序都会使用线程。当JVM启动时,它将为JVM的内部任务建立后台线程,并建立一个主线程来运行main方法。AWT和Swing的用户界面框架将建立线程来管理用户界面事件。Timer将建立线程来执行延迟任务。一些组件框架,例如Servlet和RMI,都会建立线程池并调用这些线程中的方法。

    若是要使用这些功能。那么就必须熟悉并发性和线程安全性,由于这些框架将建立线程而且在这些线程中调用程序中的代码。虽然将并发性认为是一种“可选的”或者“高级的”语言功能当然理想,但现实的状况是,几乎全部的Java应用程序都是多线程的,所以在使用这些框架时仍然须要对应用程序状态的访问进行协同。

    当某个框架在应用程序中引入并发性时,一般不可能将并发性仅局限于框架代码,由于框架自己会回调(Callback)应用程序的代码,而这些代码将访问应用程序的状态。一样,对线程安全性的需求也不能局限于被调用的代码,而是要延伸到须要访问这些代码所访问的程序状态的全部代码路径。所以,对线程安全性的需求将在程序中蔓延开来。

框架经过在框架线程中调用应用程序代码将并发性引入到程序中。在代码中将不可避免地访问应用程序状态,所以全部访问这些状态的代码路径都必须是线程安全的。

相关文章
相关标签/搜索