第一章 Java 并发编程实践基础

1.1 进程与线程

1.1.1 进程

1.1.1.1程序与资源共享
1. 程序的封闭性与可再现性
 (1)一个程序在机器中运行时独占全机资源,因此除了初始状态外,只有程序本身规定的动作才能改变这些资源的状态。
 (2)机器严格地顺序执行程序规定的动作。每个动作都必须在前一动作结束后才能开始,除了人为干预造成机器暂时停顿外,前一动作的结束就意味着后一动作的开始。程序和
机器执行程序严格一一对应。
  (3)程序的执行结果与它的运行速度无关。也就是说,处理机在执行程序两个动作之间的停顿不会影响程序的执行结果。

 闭性指的是程序一旦开始运行,其计算结果就只取决于程序本身,除了人为地改变机器的运行状态或机器故障以外,没有其它因素能够对程序的运行过程施加影响。再现性就是当机器在同一数据集上重复执行同一程序时,机器内部的动作系列完全相同,最后获得的结果也相同。这种工作方式的特点是简单、清晰、便于调试程序。

2. 资源共享与并行

多道程序技术

(1)资源共享

资源共享指的是系统中的软、硬件资源不再为单个用户程序独占,而由几道用户程序共同使用。于是,这些资源的状态就不再取决于一道程序,而是由多道程序的活动所决定。这就从根本上打破了了一道程序封闭于一个系统中运行的局面。

(2)程序的并发运行

系统中各个部分不再以单纯的串行方式工作。换言之,在任一时刻系统中不再只有一个活动,而是存在着许多并行的活动。从硬件方面看,处理机、各种外设、存储部件常常并行地进行着工作。从程序方面看,则可能有若干个作业程序或者同时、或者互相穿插在系统中并行运行。这时,机器不再是简单地顺序执行一道程序。也就是说,一道程序的前一动作结
束后,系统不一定立即执行其后续操作,而可能转而执行其它程序的某一操作。对于程序中可以执行的操作也可能不需要等待另一操作结束,系统就开始执行它们。这样也就打破了程序执行的顺序性。同时,多个程序活动可能是在不同的数据集上执行同一个程序,所以程序以及机器执行程序的活动不再有严格的一一对应关系。

1.1.1.2进程与并发

1. 进程的引入

程序活动不再处于一个封闭的系统中,而是和其它程序活动之间存在着相互依赖和制约的关系,因而呈现出并发、动态以及相互制约这些新的特征。

 进程是可以并行运动的计算部分(S.E.Madnick,J.J.Donovan);进程是一个程序与其数据一道在计算机上顺序执行时所产生的活动(A.C.Shaw);从调度组织角度出发,  称进程是一个独立的可以调度的活动(Ellis.Cohen,DavidJofferson);从资源共享和竞争方面观察, 认为进程是一个抽象的实体, 当它执行一个任务时将要求分配和释放各种资源(Peterdenning)。这些描述都注意到了进程的动态性质,但侧重面不同。为了突出进程和程序两个概念的区别和联系,对进程作如下描述:进程是一种活动,它是由一个动作系列组成,  每个动作是在某个数据集上执行一段程序, 整个活动的结果是提供一种系统或用户功能。
2.进程与程序的区别

1)进程是程序的一次运行活动,属于一种动态的概念。程序是一组有序的静态指令,是一种静态的概念。但是,进程离开了程序也就没有了存在的意义。因此,可以说:进程是执行程序的动态过程,而程序是进程运行的静态文本。

2)一个进程可以执行一个或多个程序。同一程序也可能由多个进程同时执行,在多处理机情况下,几个进程却完全可以同时使用一个程序副本。
3)程序可以作为一种软件资源长期保持着,而进程则是一次执行过程,它是暂时的,是动态地产生和终止的。
进程需要使用一种机构才能执行程序,这种机构称之为处理机(Processor)。处理机执行指令,根据指令的性质,处理机可以单独用硬件或软、硬件结合起来构成。如果指令是机器
指令,那么处理机就是我们一般所说的中央处理机(CPU)。

3.进程的并发性和不确定性
并发性:并发可以看成是在系统中同时有几个进程在活动着,也就是同时存在几个程序的执行过程。如果进程数与处理机数相同,则每个进程都占用一个处理机。但更一般的情况
是是处理机数少于进程数,于是处理机就应被共享,在进程间进行切换使用。如果相邻两次切换的时间间隔非常短,而观察时间又相当长,那么各个进程都在前进,造成一种宏观上并
行运行的效果。所以并发处理的真正含义是:如果我们把系统作为一个整体来观察,则在任一时刻有若干进程存在于系统的这一部分或那一部分,这些进程都处在其起点和终点之间。我们把所有这些进程都看成是正在系统中运行着、活跃着。

不确定性:把进程看成是一个动作系列,而每个动作是执行一段程序。处理机要检测是否已接获某种需要立即处理的中断信号。如果已经接到这种信号,则立即停止正在执行的程序段,转而执行相应的中断处理程序。在此以后,还要按情况或者恢复继续执行被中断的程序,或者调度执行另一个进程的程序。因为中断发生的时间以及频繁程度与系统中许多经常变化着的不确定因素有关,例如,系统中活跃着的进程的数量以及它们的工作情况,各种硬件工作速度的细微变化等,所有它们都是不可预测的。因此,各个进程(也就是各个动作序列)也就在不可预测的次序中前进。如果由于进程间相互制约关系造成了某一进程或某些进程异常情况,那么由于这种制约关系是与一定的活动序列紧密相关的,而这种动作序列又不易复现。于是它所造成的进程的异常运行情况也就不易复现。可见,操作系统外部表现出来的不确定性就是内部动作序列不可预测、不易复现的反应



4. 进程的结构

在 UNIX 或者 Linux 中,进程是通过 FORK 系统调用被创建的。在调用了 FORK 之后,父进程可以和子进程并行。父进程还可以创建多个子进程,也就是说,在同一时刻,一个父进程可以有多个正在运行的子进程。子进程也可以执行 FORK 调用。这样就可以在系统中生成一个进程树。

 进程通常由三部分组成。一部分是程序,一部分数据集合,另一部分被称为进程控制块(ProcessControlBlock,简记 PCB)。

进程的程序部分描述了进程所要完成的功能。数据集合部分则有两方面的内容,即程序运行时所需要的数据部分和工作区。如果一个程序能为多个进程同时共享执行,它是进程执
行时不可修改的部分。而数据集合部分则通常为一个进程独占,为进程的可修改部分。程序和数据集合是进程存在的物质基础,是进程的实体。
进程控制块有时也称为进程描述块,  它包含了进程的描述信息和控制信息, 是进程动态特性的集中反映。  它所包含的信息类型和数量随操作系统而异。在小型的比较简单的操作系统中,PCB 只占用十几个单元,而在比较复杂的大型操作系统中,PCB 则可能占用数十甚至数百个单元。
 总之,每个进程基本上有自己独立的代码和数据空间, 独立的程序计数器等上下文环境,进程切换的开销是比较大的。

1.1.2 线程

进程具备并发性的特点, 这种并发性是不同的进程之间反映出来的, 不同的进程有不同进程空间,进程之间的切换消耗比较大。在进程的内部引入并发性,一个进程可以创建多个线程,线程之间具备并发性。不同的线程之间可以共享进程的地址空间和数据。
一般的讲,线程是一个程序, 或者进程内部的一个顺序控制流。 线程本身不能独立运行,必须在进程中执行,使用进程的地址空间。每个线程有自己单独的程序计数器。
一个进程内部包含多个顺序控制流,或者并发执行多种运算,就是多线程。
 每个程序执行时都会产生一个进程,  而每一个进程至少要有一个主线程。这个线程其实是进程执行的一条线索(Thread),除了主线程外还可以给进程增加其它的线程,也即增加其它的执行线索, 由此在某种程度上可以看成是给一个应用程序增加了多任务功能。  当程序运行后,您可以根据各种条件挂起或运行这些线程,尤其在多 CPU 的环境中,这些线程
是可以并发或者并行运行的。
 多线程就是在一个进程内有多个线程。  从而使一个应用程序有了多任务的功能。创建进程的高消耗   (每个进程都有独立的数据和代码空间) ,进程之间通信的不方便(消息机制) ,进程切换的时间太长,这些导致了多线程的提出。线程切换是有代价的,因此如果采用多进程,那么就需要将线程所隶属的该进程所需要的内存进行切换,这时间代价是很多的。而线程切换代价就很少,线程是可以共享内存的。所以采用多线程在切换上花费的比多进程少得多。但是,线程切换还是需要时间消耗的。  所以采用一个拥有两个线程的进程执行所需要的时间比一个线程的进程执行两次所需要的时间要多一些。即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,  可以减少用户的响应时间。上述结果只是针对单 CPU, 如果对于多 CPU或者 CPU 采用超线程技术的话,采用多线程技术还是会提高程序的执行速度的。因为单线程只会映射到一个 CPU 上,而多线程会映射到多个 CPU 上,超线程技术本质是多线程硬件化,所以也会加快程序的执行速度。
总之,  进程内的同一类线程可以共享代码和数据空间,   每个线程有独立的运行栈和程序计数器,切换的开销比较小,灵活性高。在支持超线程和多核的 CPU 上,多线程能够并发或者并行执行,可以在同一时间段内完成不同的任务,或者加快程序的执行。同一进程内的多个线程,调度比较灵活,可以相互协调和协作共同完成特定任务。

1.2 创建多线程
 Java 定义了一个线程的概念模型,把一个线程分为三部分:虚拟 CPU(java.lang.Thread 类),虚拟 CPU 执行的代码和数据。 创建一个 java.lang.Thread 的对象,就意味着创建了一个线程。一个由 main 方法开始执行的 Java 程序,至少包含一个线程,即主线程。创建多个 Thread 的对象,就创建了多个线程。
 Thread 类通过其 run()方法来完成起任务,方法 run()为线程体。一般在 java 中有两种比较典型的构造线程的方法: 1)继承 Thread 类, 重写 run()方法; 2)把线程体从 Thread类中独立出来,形成单独的线程目标对象,就是实现 Runnable 接口及其 run()方法。
这两种方法都是通过 Thread 类的 start()方法启动线程的。
JDK5.0 提供了创建线程池并执行线程的方法。

1.2.1 继承 Thread 创建线程

这种创建方式,把线程执行的逻辑代码直接写在了 Thread 的子类中,这样根据线程的概念模型,虚拟 CPU 和代码混合在一起了。并且 java 是单继承机制,线程体继承 Thread类后,就不能继承其他类了,线程的扩展受影响。
1.2.2 实现 Runnable 接口创建线程
为了构建结构清晰线程程序,     可以把代码独立出来形成线程目标对象,然后传给 Thread对象。通常,实现 Runnable 接口的类创建的对象,称作线程的目标对象。
1.2.3 线程池
线程有时称为轻量级进程。与进程一样,它们拥有通过程序运行的独立的并发路径,并且每个线程都有自己的程序计数器,称为堆栈和本地变量。然而,线程存在于进程中,它们与同一进程内的其他线程共享内存、文件句柄以及每进程状态。
一个进程中的线程是在同一个地址空间中执行的,所以多个线程可以同时访问相同对象,并且它们从同一堆栈中分配对象。
创建线程会使用相当一部分内存,其中包括有堆栈,以及每线程数据结构。如果创建过多线程,其中每个线程都将占用一些 CPU 时间,结果将使用许多内存来支持大量线程,每个线程都运行得很慢。这样就无法很好地使用计算资源。
Java 自从 5.0 以来,提供了线程池。线程的目标执行对象可以共享线程池中有限书目的线程对象。
  一般的服务器都需要线程池,比如 Web、FTP 等服务器,不过它们一般都自己实现了线程池,比如 Tomcat、Resin 和 Jetty 等,现在 JDK 本身提供了,我们就没有必要重复造车轮
了,直接使用就可以,何况使用也很方便,性能也非常高。

 使用 JDK 提供的线程池一般分为 3 步:1)创建线程目标对象,可以是不同的;2) 使用 Executors 创建线程池,返回一个 ExecutorService类型的对象;3)使用线程池执行线程目标对象,exec.execute(run),最后,结束线程池中的线程,exec.shutdown()。



API:java.util.concurrent.Executors extends Object
该类主要定义了一些工厂方法和工具方法,其中最重要的就是创建各种线程池。

1) public static ExecutorService newFixedThreadPool(int nThreads)

创建一个可重用固定线程数的线程池, 以共享的无界队列方式来运行这些线程, 在需要时使用提供的 ThreadFactory 创建新线程。在任意点,在大多数 nThreads 线程会处于处理
任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。 如果在关闭前的执行期间由于失败而导致任何线程终止, 那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

 2) public static ThreadFactory defaultThreadFactory()
 返回用于创建新线程的默认线程工厂。此工厂创建同一个线程组(ThreadGroup)中Executor 使用的所有新线程。  如果有 SecurityManager,   则它使用 System.getSecurityManager()返回的组,其他情况则使用调用 defaultThreadFactory 方法的组。每个新线程都作为非守护程序而创建,并且具有设置线程优先级为Thread.NORM_PRIORITY 与线程组中允许的最大优先级的较小者。新线程具有可通过 pool-N-thread-M 的 Thread.getName() 来访问的名称,其中 N 是此工厂的序列号,M 是此工厂所创建线程的序列号。

3) public static ExecutorService newCachedThreadPool()
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言, 这些线程池通常可提高程序性能。 调用 execute 将
重用以前构造的线程(如果线程可用) 。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲
的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor 构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。
4) public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
5) void execute(Runnable    command)
在未来某个时间执行给定的命令。  该命令可能在新的线程、 已入池的线程或者正调用的线程中执行,这由 Executor 实现决定。
6) void shutdown()
启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。

1.3 线程的基本控制

线程创建后,可以执行 start()方法启动线程,根据线程任务的特性和线程之间的协调性要求,需要对线程进行控制。对线程的控制通常是通过调用 Thread 对象的方法实现的,主要有 sleep()、suspend()、resume()、join()、interrupt()和 stop 方法。一般情况下方法的调用会引起线程状态的转变。

1.3.1 使用 Sleep 暂停执行

Thread.sleep()使当前线程的执行暂停一段指定的时间, 这可以有效的使应用程序的其他线程或者运行在计算机上的其他进程可以使用处理器时间。该方法不会放弃除 CPU 之外的其它资源
Sleep 有两个重载的版本,一个以毫秒指定睡眠时间,另一个以纳秒指定睡眠时间,但并不保证这些睡眠时间的精确性,因为他们受到系统计时器和调度程序精度和准确性的影响。另外中断(interrupt)可以终止睡眠时间,在任何情况下,都不能假设调用 sleep 就会按照指定的时间精确的挂起线程

 sleep()方法声明可以抛出 InterruptedException 异常,当另一个线程中断了已经启动 sleep 的当前线程时机会抛出这个异常。
1.3.2 使用 join 等待另外一个线程结束

 Join 方法让一个线程等待另一个线程的完成,如果 t1,t2 是两个 Thread 对象,在 t1 中调用 t2.join(),会导致 t1 线程暂停执行,直到 t2 的线程终止。Join 的重载版本允许程序员指定等待的时间,但是和 sleep 一样,这个时间是不精确的。
1.3.3 使用中断(Interrupt)取消线程

已经启动的线程是活跃的,即 isAlive()方法返回 true,线程终止之前一直是活跃的。有三种方法可以使线程终止:1)run()方法正常返回;2)run()方法意外结束;3)应用程序终止。
通过中断(Thread.interrupt)线程来请求取消,并且让线程来监视并响应中断。中断请求通常是用户希望能够终止线程的执行, 但并不会强制终止线程,但是它会中断线程的睡眠状态,比如调用 sleep 和 wait 方法后。 线程自己检查中断状态并终止线程比直接调用 stop()放要安全很多,因为线程可以保存自己的状态。并且 stop()方法已经不推荐使用了。
和中断线程有关的方法有:1)interrupt,向线程发送中断,2)isInterrupted,测试线程是否已经被中断;3)Interrupted,测试当前线程是否已经被中断,随后清楚线程“中断”状态的静态方法。

线程的中断状态只能有线程自己清除,当线程侦测到自己被中断时,经常需要在响应中断之前做某些清除工作,这些清除工作可能涉及那些在线程仍然保持中断状态时会受到影响
的操作。

如果被中断的线程正在执行 sleep,或者 wait 方法,就会抛出 InterruptedException 异常。这种抛出异常的中断会清除线程的中断状态。

大体上任何执行阻塞操作的方法,都应该通过 Interrupt 来取消阻塞操作。

如果一个线程长时间没有调用能够抛出 InterruptedException 异常的方法,  那么线程就必须定期的调用 Thread.interrupted 方法,如果接收到中断就返回 true,然后就可以退出线程。

在更加复杂的应用程序中,当线程收到中断信号后,  抛出 InterruptedException 异常可能更有意义。把中断处理代码集中在 catch 子句中。

1.3.4 使用 Stop 终止线程

在 Thread 类中提供了 Stop 方法了强迫线程停止执行。但是现在已经过时了。

 该方法具有固有的不安全性。用 Thread.stop 来终止线程将释放它已经锁定的所有监视器(作为沿堆栈向上传播的未检查 ThreadDeath 异常的一个自然后果)   。如果以前受这些监视器保护的任何对象都处于一种不一致的状态,  则损坏的对象将对其他线程可见, 这有可能导致任意的行为。stop 的许多使用方式都应由只修改某些变量以指示目标线程应该停止运行的代码来取代。目标线程应定期检查该变量,并且如果该变量指示它要停止运行,则从其运行方法依次返回。如果目标线程等待很长时间(例如基于一个条件变量)  ,则应使用interrupt 方法来中断该等待。

无论该线程在做些什么,它所代表的线程都被迫异常停止,并抛出一个新创建的ThreadDeath 对象作为异常。停止一个尚未启动的线程是允许的。如果最后启动了该线程,它会立即终止。

应用程序通常不应试图捕获 ThreadDeath, 除非它必须执行某些异常的清除操作 (注意,抛出 ThreadDeath 将导致 try 语句的 finally 子句在线程正式终止前执行) 如果 catch 子句捕
获了一个 ThreadDeath 对象,则重新抛出该对象很重要,因为这样该线程才会真正终止。
1.3.5 结束程序的执行

每个应用程序都从执行 main 的线程开始的,如果应用程序没有创建任何其他的线程,那么 main 方法返回时,应用程序就结束了,但是如果应用程序创建了其他线程,就要根据线程的类型分情况来考虑了。
线程一般分为两种: 用户线程和守护线程。 用户线程的存在可以使应用程序保持运行状态,而守护线程则不会。当最后一个用户线程结束时,所有守护线程都会被终止,应用程序也随之结束。守护线程的终止,很像调用 destroy 所产生的终止,事发突然,没有机会做任何清楚,所以应该考虑清楚,用守护线程执行哪种类型的任务。使用 Thread.setDaemon(true)
可以把线程标记为守护线程。默认情况下,线程的守护状态继承自创建它的线程。  一般 main 线程是程序运行时第一个启动的线程,称作初始线程。如果希望应用程序在初始线程消亡后就退出,就可以把所有创建出来的线程都标记为守护线程。
可以通过调用 System,或者 Runtime 的 exit 方法来强制应用程序结束,这个方法将终止 Java 虚拟机的当前执行过程。
 许多类会隐式的在应用程序中创建线程, 比如图形用户界面, 并创建了特殊的线程来处理事件。有些是守护线程,有些不是。如果没有更好的办法,那么就可以用 exit 方法。









  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值