Java并发编程实战 笔记(一) 简介

来源:互联网 发布:mac 简体双拼输入法 编辑:程序博客网 时间:2024/05/22 11:37

并发早已经不是高级用户谈论的话题,作为一准应届毕业生,也会在面试中遇到很多并发的问题,所以在这里记下读书笔记。

第一章


1. 并发为什么会产生问题

大多数现在操作系统中,都是以线程为基本的调度单位,而不是进程。如果没有明确的协同机制,那么线程将彼此独立执行。由于同一个进程中的所有线程都将共享进程的内存地址空间,因此这些线程都能访问相同的变量并在同一个堆上分配对象,这就需要实现一种比在进程间共享数据粒度更细的数据共享机制。如果没有明确的同步机制来协同对共享数据的访问,那么当一个线程正在使用某个变量时,另一个线程可能同时访问这个变量,这将造成不可预测的结果。

2. 线程带来的风险

2.1 安全性问题

在没有充足同步的情况下,多个线程的操作执行顺序是不可预测的,甚至会产生奇怪的结果。例如:

        public class UnsafeSequence{            private int value;            public int getNext(){                return value++;            }        }        //main函数执行两次,结果可能是同一个数        getNext();        getNext();

虽然value++看上去像一个单个操作,但事实上它包含三个独立的操作:读取value,将value加1,并将计算结果写入value。
由于运行时可能将多个线程之间的操作交替执行,因此可能在线程1修改的value还未更新到内存里,这时线程2就过来读取value,导致线程2读取的是还未更新的value。

在UnsafeSequence类中说明的是一种常见的并发安全问题,称为竞态条件。在多线程环境下,getValue是否会返回唯一的值,要取决于运行时对线程中操作的交替执行方式,这不是我们想看到的。

幸运的是,Java提供了各种同步机制来协同这种访问。通过将getNext修改为一个同步方法,可以修复UnsafeSequence中的错误。 
  

        public class Sequence{           private int value;           public synchronized int getNext(){               return value++;           }      }

(但是这种方法过于极端,因为多个客户端无法同时使用这个方法,导致效率低下)

2.2活跃性问题

安全性不仅对于多线程序很重要,对于单线程程序同样重要。此外,线程还会导致一些在单线程程序中不会出现的问题,例如活跃性问题。

安全性的含义是『永远不会发生糟糕的事情』,而活跃性则关注『某件正确的事情最终会发生』。当某个操作系统无法继续执行下去时,就会发生活跃性问题。在串行程序中,活跃性问题的形式之一就是无意中造成的无限循环,从而使循环制后的代码无法得到执行。线程将带来其他一些活跃性问题。例如,若果线程A在等待线程B释放其持有的资源,而线程B永远都不释放该资源,那么A就会永久地等待下去。

2.3性能问题

性能问题代表某件事最终会发生,但却不够好,不能紧缺的发生。例如服务时间过长,响应不灵敏,吞吐率过低,或者可伸缩性较低等。

在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总会带来某种程度的运行时开销。在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作(Context Switch),这种操作将带来极大的开销,CPU时间将更多地花在线程调度而不是线程运行商上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,是内存缓存区中的数据无效,以及增加共享内存总线的同步流量。

3.线程无处不在

每个Java应用程序都会使用线程,当JVM启动时,它将为JVM的内部任务(列入,垃圾收集、终结操作等)创建后台线程,并创建一个主线程来运行main方法。一些组件框架,例如Servelet和RMI(Remote Method Invocation),都会创建线程池并调用这些线程中的方法。
  
当某个框架在应用程序(使用者写的代码)中引入并发性时,通常不可能将并发性局限于框架本身的代码里,因为框架本身会回调(Callback)应用程序的代码,而这些代码将访问应用程序的状态。同样,对线程安全性的需求也不能局限于被调用的代码,而是要延伸到需要访问这些代码所访问的程序状态的所有代码路径。因此,对线程安全性的需求将在程序中蔓延开来。
  
下面给出的模块都将在应用程序之外的线程中(这里指框架内部的线程)调用应用程序的代码(这里指使用者写的代码)。尽管线程安全性需求可能源自这些模块,但却不会止步于他们,而是会延伸到整个应用程序。
  
Timer。Timer类的作用是使任务在稍后的时刻运行,或者运行一次,或者周期性地运行。引入Timer可能会使串行程序变得更复杂,因为TimerTask将在Timer管理的线程中执行,而不是由应用程序来管理。如果某个TimerTask访问了应用程序中其他线程访问的数据,那么不仅TimerTask需要以线程安全的方式来访问数据,其他类也必须采用线程安全的方式来访问该数据。通常,要实现这个目标,最简单的方式是确保TimerTask访问的对象本身是线程安全的,从而就能把线程安全性封装在共享对象内部。
  
Servlet和JSP。Servlet框架用于部署网页应用程序以及分发来自HTTP客户端的请求。到每个Servlet都表示一个程序逻辑组件,在高吞吐率网站中,多个客户端可能同时请求同一个Servlet的服务。在Servlet规范中,Servlet同样需要满足被多个线程同时调用,换句话说,Servlet需要是线程安全的。
  
当一个Servlet访问 在 多个Servlet或者请求中 共享的对象,必须正确地协同对这些对象的访问,因为多个请求可能在不同的线程中同时访问这些对象。
  
远程方法调用(Remote Method Invocation,RMI)。RMI使代码能够调用在其他JVM中运行的对象。当通过RMI调用某个远程方法时,传递给方法的参数必须被打包到一个字节流中,通过网络传输给远程JVM,然后由远程JVM拆包并传递给远程方法(这里的远程方法、远程对象都是指其他JVM里的)
  
当RMI代码(是使用者写的)调用远程对象时,这个调用将在哪个线程中执行?你并不知道,但肯定不会是在你创建的线程中,而是将在一个由RMI管理的线程中调用对象。
  
远程对象必须注意两个线程安全性问题:①正确的协同在多个对象中共享的状态,以及对远程对象本身状态的访问(由于同一个对象可能会在多个线程中被同时访问)②与Servlet相同,RMI对象应该做好被多个线程同时调用的准备,并且必须确保他们自身的线程安全性。
  
Swing和AWT。GUI应用程序的一个固有属性是异步性。用户可以在任意时刻选择一个菜单项或按下一个按钮,应用程序就会及时响应,即使应用程序当时正在执行其他的任务。Swing和AWT很好的解决了这个问题,他们创建了一个单独的线程(事件线程)来处理用户触发的事件,并对呈献给用户的图形界面进行更新。
  
Swing的一些组件并不是线程安全的,例如JTable。相反,Swing程序通过将所有对GUI组件的访问局限在事件线程中,以实现线程安全性。如果某些应用程序希望在事件线程之外控制GUI,那么必须将控制GUI的代码放在事件线程中运行。
  
当用户触发某个UI动作时,在事件线程中就会有一个事件处理器被调用以执行用户请求的操作。如果事件处理器需要访问一个其他线程正在访问的应用程序状态(例如编辑某个文档),那么这个事件处理器,以及访问这个状态(储存在状态变量(例如实例或静态域)中的数据)的所有其他代码,都必须采用一种线程安全的方式来访问这种状态。
  

0 0