2016-2-22 by Damon
java
下面的这个简单的java程序完成四项不相关的任务。这样的程序有单个控制线程,控制在这四个任务之间线性地移动。此外。由于所需的资源-打印机、磁盘、数据库和显示屏--因为硬件和软件的限制都有内在的潜伏时间,因此每项任务都包含明显的等待时间。所以,程序在访问数据库以前必须等待打印机完成打印文件的任务,等等。若是您正在等待程序的完成,则这是对计算机资源和您的时间的一种拙劣使用。改进此程序的一种方法是使它成为多线程的。程序员
四项不相关的任务算法
class myclass { static public void main(String args[]) { print_a_file(); manipulate_another_file(); access_database(); draw_picture_on_screen(); } }
在本例中,每项任务在开始以前必须等待前一项任务完成,即便所涉及的任务绝不相关也是这样。可是,在现实生活中,咱们常用多线程模型。咱们在处理某些任务的同时也可让孩子、配偶和父母完成别的任务。例如,我在写信的同时可能打发个人儿子去邮局买邮票。用软件术语来讲,这称为多个控制(或执行)线程。数据库
能够用两种不一样的方法来得到多个控制线程:编程
多个进程安全
在大多数操做系统中均可以建立多个进程。当一个程序启动时,它能够为即将开始的每项任务建立一个进程,并容许它们同时运行。当一个程序因等待网络访问或用户输入而被阻塞时,另外一个程序还能够运行,这样就增长了资源利用率。可是,按照这种方式建立每一个进程要付出必定的代价:设置一个进程要占用至关一部分处理器时间和内存资源。并且,大多数操做系统不容许进程访问其余进程的内存空间。所以,进程间的通讯很不方便,而且也不会将它本身提供给容易的编程模型。服务器
线程网络
线程也称为轻型进程 (LWP)。由于线程只能在单个进程的做用域内活动,因此建立线程比建立进程要廉价得多。这样,由于线程容许协做和数据交换,而且在计算资源方面很是廉价,因此线程比进程更可取。线程须要操做系统的支持,所以不是全部的机器都提供线程。Java 编程语言,做为至关新的一种语言,已将线程支持与语言自己合为一体,这样就对线程提供了强健的支持。多线程
Java 编程语言使多线程如此简单有效,以至于某些程序员说它其实是天然的。尽管在 Java 中使用线程比在其余语言中要容易得多,仍然有一些概念须要掌握。要记住的一件重要的事情是 main() 函数也是一个线程,并可用来作有用的工做。程序员只有在须要多个线程时才须要建立新的线程。并发
Thread 类是一个具体的类,即不是抽象类,该类封装了线程的行为。要建立一个线程,程序员必须建立一个从 Thread 类导出的新类。程序员必须覆盖 Thread 的 run() 函数来完成有用的工做。用户并不直接调用此函数;而是必须调用 Thread 的 start() 函数,该函数再调用 run()。下面的代码说明了它的用法:
建立两个新线程
import java.util.*; class TimePrinter extends Thread { int pauseTime; String name; public TimePrinter(int x, String n) { pauseTime = x; name = n; } public void run() { while(true) { try { System.out.println(name + ":" + new Date(System.currentTimeMillis())); Thread.sleep(pauseTime); } catch(Exception e) { System.out.println(e); } } } static public void main(String args[]) { TimePrinter tp1 = new TimePrinter(1000, "Fast Guy"); tp1.start(); TimePrinter tp2 = new TimePrinter(3000, "Slow Guy"); tp2.start(); } }
在本例中,咱们能够看到一个简单的程序,它按两个不一样的时间间隔(1 秒和 3 秒)在屏幕上显示当前时间。这是经过建立两个新线程来完成的,包括 main() 共三个线程。可是,由于有时要做为线程运行的类可能已是某个类层次的一部分,因此就不能再按这种机制建立线程。虽然在同一个类中能够实现任意数量的接口,但 Java 编程语言只容许一个类有一个父类。同时,某些程序员避免从 Thread 类导出,由于它强加了类层次。对于这种状况,就要 runnable 接口。
此接口只有一个函数,run(),此函数必须由实现了此接口的类实现。可是,就运行这个类而论,其语义与前一个示例稍有不一样。咱们能够用 runnable 接口改写前一个示例。(不一样的部分用黑体表示。)
建立两个新线程
import java.util.*; class TimePrinter implements Runnable { int pauseTime; String name; public TimePrinter(int x, String n) { pauseTime = x; name = n; } public void run() { while(true) { try { System.out.println(name + ":" + new Date(System.currentTimeMillis())); Thread.sleep(pauseTime); } catch(Exception e) { System.out.println(e); } } } static public void main(String args[]) { Thread t1 = new Thread (new TimePrinter(1000, "Fast Guy")); t1.start(); Thread t2 = new Thread (new TimePrinter(3000, "Slow Guy")); t2.start(); } }
请注意,当使用 runnable 接口时,您不能直接建立所需类的对象并运行它;必须从 Thread 类的一个实例内部运行它。许多程序员更喜欢 runnable 接口,由于从 Thread 类继承会强加类层次。
到目前为止,咱们看到的示例都只是以很是简单的方式来利用线程。只有最小的数据流,并且不会出现两个线程访问同一个对象的状况。可是,在大多数有用的程序中,线程之间一般有信息流。试考虑一个金融应用程序,它有一个 Account 对象,以下例中所示:
一个银行中的多项活动
public class Account { String holderName; float amount; public Account(String name, float amt) { holderName = name; amount = amt; } public void deposit(float amt) { amount += amt; } public void withdraw(float amt) { amount -= amt; } public float checkBalance() { return amount; } }
在此代码样例中潜伏着一个错误。若是此类用于单线程应用程序,不会有任何问题。可是,在多线程应用程序的状况中,不一样的线程就有可能同时访问同一个 Account 对象,好比说一个联合账户的全部者在不一样的 ATM 上同时进行访问。在这种状况下,存入和支出就可能以这样的方式发生:一个事务被另外一个事务覆盖。这种状况将是灾难性的。可是,Java 编程语言提供了一种简单的机制来防止发生这种覆盖。每一个对象在运行时都有一个关联的锁。这个锁可经过为方法添加关键字 synchronized 来得到。这样,修订过的 Account 对象(以下所示)将不会遭受像数据损坏这样的错误:
对一个银行中的多项活动进行同步处理
public class Account { String holderName; float amount; public Account(String name, float amt) { holderName = name; amount = amt; } public synchronized void deposit(float amt) { amount += amt; } public synchronized void withdraw(float amt) { amount -= amt; } public float checkBalance() { return amount; } }
deposit() 和 withdraw() 函数都须要这个锁来进行操做,因此当一个函数运行时,另外一个函数就被阻塞。请注意, checkBalance() 未做更改,它严格是一个读函数。由于 checkBalance() 未做同步处理,因此任何其余方法都不会阻塞它,它也不会阻塞任何其余方法,无论那些方法是否进行了同步处理。
线程是被个别建立的,但能够将它们归类到 线程组中,以便于调试和监视。只能在建立线程的同时将它与一个线程组相关联。在使用大量线程的程序中,使用线程组组织线程可能颇有帮助。能够将它们看做是计算机上的目录和文件结构。
当线程在继续执行前须要等待一个条件时,仅有 synchronized 关键字是不够的。虽然 synchronized 关键字阻止并发更新一个对象,但它没有实现 线程间发信 。Object 类为此提供了三个函数:wait()、notify() 和 notifyAll()。以全球气候预测程序为例。这些程序经过将地球分为许多单元,在每一个循环中,每一个单元的计算都是隔离进行的,直到这些值趋于稳定,而后相邻单元之间就会交换一些数据。因此,从本质上讲,在每一个循环中各个线程都必须等待全部线程完成各自的任务之后才能进入下一个循环。这个模型称为 屏蔽同步,下例说明了这个模型:
屏蔽同步
public class BSync { int totalThreads; int currentThreads; public BSync(int x) { totalThreads = x; currentThreads = 0; } public synchronized void waitForAll() { currentThreads++; if(currentThreads < totalThreads) { try { wait(); } catch (Exception e) {} } else { currentThreads = 0; notifyAll(); } } }
当对一个线程调用 wait() 时,该线程就被有效阻塞,只到另外一个线程对同一个对象调用 notify() 或 notifyAll() 为止。所以,在前一个示例中,不一样的线程在完成它们的工做之后将调用 waitForAll() 函数,最后一个线程将触发 notifyAll() 函数,该函数将释放全部的线程。第三个函数 notify() 只通知一个正在等待的线程,当对每次只能由一个线程使用的资源进行访问限制时,这个函数颇有用。可是,不可能预知哪一个线程会得到这个通知,由于这取决于 Java 虚拟机 (JVM) 调度算法。
当线程放弃某个稀有的资源(如数据库链接或网络端口)时,它可能调用 yield() 函数临时下降本身的优先级,以便某个其余线程可以运行。
有两类线程:用户线程和守护线程。 用户线程是那些完成有用工做的线程。 守护线程 是那些仅提供辅助功能的线程。Thread 类提供了 setDaemon() 函数。Java 程序将运行到全部用户线程终止,而后它将破坏全部的守护线程。在 Java 虚拟机 (JVM) 中,即便在 main 结束之后,若是另外一个用户线程仍在运行,则程序仍然能够继续运行。
不提倡使用的方法是为支持向后兼容性而保留的那些方法,它们在之后的版本中可能出现,也可能不出现。Java 多线程支持在版本 1.1 和版本 1.2 中作了重大修订,stop()、suspend() 和 resume() 函数已不提倡使用。这些函数在 JVM 中可能引入微妙的错误。虽然函数名可能听起来很诱人,但请抵制诱惑不要使用它们。
在线程化的程序中,可能发生的某些常见而讨厌的状况是死锁、活锁、内存损坏和资源耗尽。
死锁多是多线程程序最多见的问题。当一个线程须要一个资源而另外一个线程持有该资源的锁时,就会发生死锁。这种状况一般很难检测。可是,解决方案却至关好:在全部的线程中按相同的次序获取全部资源锁。例如,若是有四个资源 ―A、B、C 和 D ― 而且一个线程可能要获取四个资源中任何一个资源的锁,则请确保在获取对 B 的锁以前首先获取对 A 的锁,依此类推。若是“线程 1”但愿获取对 B 和 C 的锁,而“线程 2”获取了 A、C 和 D 的锁,则这一技术可能致使阻塞,但它永远不会在这四个锁上形成死锁。
当一个线程忙于接受新任务以至它永远没有机会完成任何任务时,就会发生活锁。这个线程最终将超出缓冲区并致使程序崩溃。试想一个秘书须要录入一封信,但她一直在忙于接电话,因此这封信永远不会被录入。
若是明智地使用 synchronized 关键字,则彻底能够避免内存错误这种气死人的问题。
某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,由于每一个线程均可能但愿有一个这样的资源。若是线程数至关大,或者某个资源的侯选线程数远远超过了可用的资源数,则最好使用 资源池。一个最好的示例是数据库链接池。只要线程须要使用一个数据库链接,它就从池中取出一个,使用之后再将它返回池中。资源池也称为 资源库。
有时一个程序由于有大量的线程在运行而极难调试。在这种状况下,下面的这个类可能会派上用场:
public class Probe extends Thread { public Probe() {} public void run() { while(true) { Thread[] x = new Thread[100]; Thread.enumerate(x); for(int i=0; i<100; i++) { Thread t = x[i]; if(t == null) break; else System.out.println(t.getName() + "\t" + t.getPriority() + "\t" + t.isAlive() + "\t" + t.isDaemon()); } } } }
Java 线程模型涉及能够动态更改的线程优先级。本质上,线程的优先级是从 1 到 10 之间的一个数字,数字越大代表任务越紧急。JVM 标准首先调用优先级较高的线程,而后才调用优先级较低的线程。可是,该标准对具备相同优先级的线程的处理是随机的。如何处理这些线程取决于基层的操做系统策略。在某些状况下,优先级相同的线程分时运行;在另外一些状况下,线程将一直运行到结束。请记住,Java 支持 10 个优先级,基层操做系统支持的优先级可能要少得多,这样会形成一些混乱。所以,只能将优先级做为一种很粗略的工具使用。最后的控制能够经过明智地使用 yield() 函数来完成。一般状况下,请不要依靠线程优先级来控制线程的状态。
本文说明了在 Java 程序中如何使用线程。像是否 应该使用线程这样的更重要的问题在很大程序上取决于手头的应用程序。决定是否在应用程序中使用多线程的一种方法是,估计能够并行运行的代码量。并记住如下几点:
使用多线程不会增长 CPU 的能力。可是若是使用 JVM 的本地线程实现,则不一样的线程能够在不一样的处理器上同时运行(在多 CPU 的机器中),从而使多 CPU 机器获得充分利用。
若是应用程序是计算密集型的,并受 CPU 功能的制约,则只有多 CPU 机器可以从更多的线程中受益。
当应用程序必须等待缓慢的资源(如网络链接或数据库链接)时,或者当应用程序是非交互式的时,多线程一般是有利的。
基于 Internet 的软件有必要是多线程的;不然,用户将感受应用程序反映迟钝。例如,当开发要支持大量客户机的服务器时,多线程可使编程较为容易。在这种状况下,每一个线程能够为不一样的客户或客户组服务,从而缩短了响应时间。
容许线程本身决定何时放弃处理器来等待其余的线程。程序开发员能够精确地决定某个线程什么时候会被其余线程挂起,容许它们与对方有效地合做。缺点在于某些恶意或是写得很差的线程会消耗全部可得到的 CPU 时间,致使其余线程“饥饿”。
操做系统能够在任什么时候候打断线程。一般会在它运行了一段时间(就是所谓的一个时间片)后才打断它。这样的结果天然是没有线程可以不公平地长时间霸占处理器。然而,随时可能打断线程就会给程序开发员带来其余麻烦。一样使用办公室的例子,假设某个职员抢在另外一人前使用复印机,但打印工做在未完成的时候离开了,另外一人接着使用复印机时,该复印机上可能就还有先前那名职员留下来的资料。抢占式线程模型要求线程正确共享资源,协做式模型却要求线程共享执行时间。因为 JVM 规范并无特别规定线程模型,Java 开发员必须编写可在两种模型上正确运行的程序。在了解线程以及线程间通信的一些方面以后,咱们能够看到如何为这两种模型设计程序。
死锁
死锁是一个经典的多线程问题,由于不一样的线程都在等待那些根本不可能被释放的锁,从而致使全部的工做都没法完成。假设有两个线程,分别表明两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都须要得到两个锁:共享刀和共享叉的锁。假如线程 "A" 得到了刀,而线程 "B" 得到了叉。线程 A 就会进入阻塞状态来等待得到叉,而线程 B 则阻塞来等待 A 所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类状况却时常发生。虽然要探测或推敲各类状况是很是困难的,但只要按照下面几条规则去设计系统,就可以避免死锁问题:
让全部的线程按照一样的顺序得到一组锁。这种方法消除了 X 和 Y 的拥有者分别等待对方的资源的问题。
将多个锁组成一组并放到同一个锁下。前面死锁的例子中,能够建立一个银器对象的锁。因而在得到刀或叉以前都必须得到这个银器的锁。
将那些不会阻塞的可得到资源用变量标志出来。当某个线程得到银器对象的锁时,就能够经过检查变量来判断是否整个银器集合中的对象锁均可得到。若是是,它就能够得到相关的锁,不然,就要释放掉银器这个锁并稍后再尝试。
最重要的是,在编写代码前认真仔细地设计整个系统。多线程是困难的,在开始编程以前详细设计系统可以帮助你避免难以发现死锁的问题。
Volatile 变量
volatile 关键字是 Java 语言为优化编译器设计的。如下面的代码为例:
class VolatileTest { public void foo() { boolean flag = false; if(flag) { //this could happen } } }
一个优化的编译器可能会判断出 if 部分的语句永远不会被执行,就根本不会编译这部分的代码。若是这个类被多线程访问, flag 被前面某个线程设置以后,在它被 if 语句测试以前,能够被其余线程从新设置。用 volatile 关键字来声明变量,就能够告诉编译器在编译的时候,不须要经过预测变量值来优化这部分的代码。
没法访问的线程
有时候虽然获取对象锁没有问题,线程依然有可能进入阻塞状态。在 Java 编程中 IO 就是这类问题最好的例子。当线程由于对象内的 IO 调用而阻塞时,此对象应当仍能被其余线程访问。该对象一般有责任取消这个阻塞的 IO 操做。形成阻塞调用的线程经常会令同步任务失败。若是该对象的其余方法也是同步的,当线程被阻塞时,此对象也就至关于被冷冻住了。其余的线程因为不能得到对象的锁,就不能给此对象发消息(例如,取消 IO 操做)。必须确保不在同步代码中包含那些阻塞调用,或确认在一个用同步阻塞代码的对象中存在非同步方法。尽管这种方法须要花费一些注意力来保证结果代码安全运行,但它容许在拥有对象的线程发生阻塞后,该对象仍可以响应其余线程。