




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、编写多线程的 Java 应用程序如何避免当前编程中最常见的问题Alex Roetter (aroetterCS.STeton Data Systems 的软件工程师2001 年 2 月Java Thread API 允许程序员编写具有多处理机制优点的应用程序在后台处理任务的同时保持用户所需的交互感Alex Roetter 介绍了Java Thread API并概述多线程可能引起的问题以及常见问题的解决方案几乎所有使用 AWT 或 Swing 编写的画图程序都需要多线程但多线程程序会造成许多困难刚开始编程的开发者常常会发现他们被一些问题所折磨例如不正确的程序行为或死锁在本文
2、中我们将探讨使用多线程时遇到的问题并提出那些常见陷阱的解决方案线程是什么 一个程序或进程能够包含多个线程这些线程可以根据程序的代码执行相应的指令多线程看上去似乎在并行执行它们各自的工作就像在一台计算机上运行着多个处理机一样在多处理机计算机上实现多线程时它们确实可以并行工作和进程不同的是线程共享地址空间也就是说多个线程能够读写相同的变量或数据结构编写多线程程序时你必须注意每个线程是否干扰了其他线程的工作可以将程序看作一个办公室如果不需要共享办公室资源或与其他人交流所有职员就会独立并行地工作某个职员若要和其他人交谈当且仅当该职员在听且他们两说同样的语言此外只有在复印机空闲且处于可用状态没有仅完成一
3、半的复印工作没有纸张阻塞等问题时职员才能够使用它在这篇文章中你将看到在 Java 程序中互相协作的线程就好像是在一个组织良好的机构中工作的职员在多线程程序中线程可以从准备就绪队列中得到并在可获得的系统 CPU 上运行操作系统可以将线程从处理器移到准备就绪队列或阻塞队列中这种情况可以认为是处理器挂起了该线程同样Java 虚拟机 (JVM 也可以控制线程的移动在协作或抢先模型中从准备就绪队列中将进程移到处理器中于是该线程就可以开始执行它的程序代码协作式线程模型允许线程自己决定什么时候放弃处理器来等待其他的线程程序开发员可以精确地决定某个线程何时会被其他线程挂起允许它们与对方有效地合作缺点在于某些恶
4、意或是写得不好的线程会消耗所有可获得的 CPU 时间导致其他线程饥饿 在抢占式线程模型中操作系统可以在任何时候打断线程通常会在它运行了一段时间就是所谓的一个时间片后才打断它这样的结果自然是没有线程能够不公平地长时间霸占处理器然而随时可能打断线程就会给程序开发员带来其他麻烦同样使用办公室的例子假设某个职员抢在另一人前使用复印机但打印工作在未完成的时候离开了另一人接着使用复印机时该复印机上可能就还有先前那名职员留下来的资料抢占式线程模型要求线程正确共享资源协作式模型却要求线程共享执行时间由于 JVM 规范并没有特别规定线程模型Java 开发员必须编写可在两种模型上正确运行的程序在了解线程以及线程间
5、通讯的一些方面之后我们可以看到如何为这两种模型设计程序线程和 Java 语言为了使用 Java 语言创建线程你可以生成一个Thread类或其子类的对象并给这个对象发送start(消息程序可以向任何一个派生自Runnable接口的类对象发送start(消息每个线程动作的定义包含在该线程对象的run(方法中run 方法就相当于传统程序中的main(方法线程会持续运行直到run(返回为止此时该线程便死了上锁大多数应用程序要求线程互相通信来同步它们的动作在 Java 程序中最简单实现同步的方法就是上锁为了防止同时访问共享资源线程在使用资源的前后可以给该资源上锁和开锁假想给复印机上锁任一时刻只有一个职员
6、拥有钥匙若没有钥匙就不能使用复印机给共享变量上锁就使得 Java 线程能够快速方便地通信和同步某个线程若给一个对象上了锁 就可以知道没有其他线程能够访问该对象即使在抢占式模型中其他线程也不能够访问此对象直到上锁的线程被唤醒完成工作并开锁那些试图访问一个上锁对象的线程通常会进入睡眠状态直到上锁的线程开锁一旦锁被打开这些睡眠进程就会被唤醒并移到准备就绪队列中在 Java 编程中所有的对象都有锁线程可以使用synchronized关键字来获得锁在任一时刻对于给定的类的实例方法或同步的代码块只能被一个线程执行这是因为代码在执行之前要求获得对象的锁继续我们关于复印机的比喻为了避免复印冲突我们可以简单地对
7、复印资源实行同步如同下列的代码例子任一时刻只允许一位职员使用复印资源通过使用方法在Copier对象中来修改复印机状态这个方法就是同步方法只有一个线程能够执行一个Copier对象中同步代码因此那些需要使用Copier 对象的职员就必须排队等候class CopyMachine public synchronized void makeCopies(Document d, int nCopies /only one thread executes this at a timepublic void loadPaper( /multiple threads could access this at
8、once!synchronized(this /only one thread accesses this at a time/feel free to use shared resources, overwrite members, etc.Fine-grain 锁在对象级使用锁通常是一种比较粗糙的方法为什么要将整个对象都上锁而不允许其他线程短暂地使用对象中其他同步方法来访问共享资源如果一个对象拥有多个资源就不需要只为了让一个线程使用其中一部分资源就将所有线程都锁在外面由于每个对象都有锁可以如下所示使用虚拟对象来上锁class FineGrainLock MyMemberClass x, y
9、;Object xlock = new Object(, ylock = new Object(;public void foo( synchronized(xlock /access x here/do something here - but don't use shared resourcessynchronized(ylock /access y herepublic void bar( synchronized(this /access both x and y here/do something here - but don't use shared resourc
10、es若为了在方法级上同步不能将整个方法声明为synchronized关键字它们使用的是成员锁而不是 synchronized 方法能够获得的对象级锁信号量通常情况下可能有多个线程需要访问数目很少的资源假想在服务器上运行着若干个回答客户端请求的线程这些线程需要连接到同一数据库但任一时刻只能获得一定数目的数据库连接你要怎样才能够有效地将这些固定数目的数据库连接分配给大量的线程一种控制访问一组资源的方法除了简单地上锁之外就是使用众所周知的信号量计数 (counting semaphore信号量计数将一组可获得资源的管理封装起来信号量是在简单上锁的基础上实现的,相当于能令线程安全执行并初始化为可用资源
11、个数的计数器例如我们可以将一个信号量初始化为可获得的数据库连接个数一旦某个线程获得了信号量可获得的数据库连接数减一线程消耗完资源并释放该资源时计数器就会加一当信号量控制的所有资源都已被占用时若有线程试图访问此信号量则会进入阻塞状态直到有可用资源被释放信号量最常见的用法是解决消费者生产者问题当一个线程进行工作时若另外一个线程访问同一共享变量就可能产生此问题消费者线程只能在生产者线程完成生产后才能够访问数据使用信号量来解决这个问题就需要创建一个初始化为零的信号量从而让消费者线程访问此信号量时发生阻塞每当完成单位工作时生产者线程就会向该信号量发信号释放资源每当消费者线程消费了单位生产结果并需要新的数
12、据单元时它就会试图 再次获取信号量因此信号量的值就总是等于生产完毕可供消费的数据单元数这种方法比采用消费者线程不停检查是否有可用数据单元的方法要高效得多因为消费者线程醒来后倘若没有找到可用的数据单元就会再度进入睡眠状态这样的操作系统开销是非常昂贵的尽管信号量并未直接被 Java 语言所支持却很容易在给对象上锁的基础上实现一个简单的实现方法如下所示class Semaphore private int count;public Semaphore(int n this.count = n;public synchronized void acquire( while(count = 0 try
13、wait(; catch (InterruptedException e /keep tryingcount-;public synchronized void release( count+;notify(; /alert a thread that's blocking on this semaphore常见的上锁问题不幸的是使用上锁会带来其他问题让我们来看一些常见问题以及相应的解决方法死锁死锁是一个经典的多线程问题因为不同的线程都在等待那些根本不可能被释放的锁从而导致所有的工作都无法完成假设有两个线程分别代表两个饥饿的人他们必须共享刀叉并轮流吃饭他们都需要获得两个锁共享刀和共享叉
14、的锁假如线程 "A" 获得了刀而线程 "B" 获得了叉线 程 A 就会进入阻塞状态来等待获得叉而线程 B 则阻塞来等待 A 所拥有的刀这只是人为设计的例子但尽管在运行时很难探测到这类情况却时常发生虽然要探测或推敲各种情况是非常困难的 但只要按照下面几条规则去设计系统就能够避免死锁问题 让所有的线程按照同样的顺序获得一组锁这种方法消除了 X 和 Y 的拥有者分别等待对方的资源的问题 将多个锁组成一组并放到同一个锁下前面死锁的例子中可以创建一个银器对象的锁于是在获得刀或叉之前都必须获得这个银器的锁 将那些不会阻塞的可获得资源用变量标志出来当某个线程获得银器对
15、象的锁时就可以通过 检查变量来判断是否整个银器集合中的对象锁都可获得如果是它就可以获得相关的锁否则就要释放掉银器这个锁并稍后再尝试 最重要的是在编写代码前认真仔细地设计整个系统多线程是困难的在开始编程之前详细 设计系统能够帮助你避免难以发现死锁的问题Volatile 变量.volatile关键字是 Java 语言为优化编译器设计的以下面的代码为例 class VolatileTest public void foo( boolean flag = false; if(flag /this could happen 一个优化的编译器可能会判断出 if 部分的语句永远不会被执行 就根本不会编译这
16、部分的代码 如果这个类被多线程访问 flag 被前面某个线程设置之后 在它被 if 语 句测试之前 可以被其他线程重新设置 用 volatile 关键字来声明变量 就可以告诉 编译器在编译的时候 不需要通过预测变量值来优化这部分的代码 无法访问的线程 有时候虽然获取对象锁没有问题 IO 就是这类问题最好的例子 访问 线程依然有可能进入阻塞状态 在 Java 编程中 当线程因为对象内的 IO 调用而阻塞时 此对象应当仍能被其他线程 该对象通常有责任取消这个阻塞的 IO 操作 当线程被阻塞时 造成阻塞调用的线程常常会令同步任务失败 如 此对象也就相当于被冷冻住了 其他的线程由于 果该对象的其他方法
17、也是同步的 不能获得对象的锁 就不能给此对象发消息 例如 取消 IO 操作 些阻塞调用 或确认在一个用同步阻塞代码的对象中存在非同步方法 必须确保不在同步代码中包含那 尽管这种方法需要花费一些注 意力来保证结果代码安全运行 但它允许在拥有对象的线程发生阻塞后 该对象仍能够响应其他线程 为不同的线程模型进行设计 判断是抢占式还是协作式的线程模型 取决于虚拟机的实现者 并根据各种实现而不同 因 此 Java 开发员必须编写那些能够在两种模型上工作的程序 正如前面所提到的 在抢占式模型中线程可以在代码的任何一个部分的中间被打断 除非那 是一个原子操作代码块 原子操作代码块中的代码段一旦开始执行 就要
18、在该线程被换出处 理器之前执行完毕 在 Java 编程中 分配一个小于 32 位的变量空间是一种原子操作 而 此外象 double 和 long 这两个 64 位数据类型的分配就不是原子的 使用锁来正确同步共享 资源的访问 就足以保证一个多线程程序在抢占式模型下正确工作 而在协作式模型中 是否能保证线程正常放弃处理器 不掠夺其他线程的执行时间 则完全 取决于程序员 调用 yield( 方法能够将当前的线程从处理器中移出到准备就绪队列中 另 一个方法则是调用 sleep( 方法 使线程放弃处理器 并且在 sleep 方法中指定的时间间隔 内睡眠 正如你所想的那样 将这些方法随意放在代码的某个地方
19、 并不能够保证正常工作 如果线 程正拥有一个锁 因为它在一个同步方法或代码块中 则当它调用 yield( 时不能够释放 这个锁 这就意味着即使这个线程已经被挂起 等待这个锁释放的其他线程依然不能继续运 行 为了缓解这个问题 最好不在同步方法中调用 yield 方法 将那些需要同步的代码包在 一个同步块中 里面不含有非同步的方法 并且在这些同步代码块之外才调用 yield 另外一个解决方法则是调用 wait( 方法 使处理器放弃它当前拥有的对象的锁 如果对象在 方法级别上使同步的 这种方法能够很好的工作 因为它仅仅使用了一个锁 如果它使用 fine-grained 锁 则 wait( 将无法放弃
20、这些锁 此外 一个因为调用 wait( 方法而阻塞的 线程 只有当其他线程调用 notifyAll( 时才会被唤醒 线程和 AWT/Swing 在那些使用 Swing 和/或 AWT 包创建 GUI 用户图形界面 的 Java 程序中 AWT 事 件句柄在它自己的线程中运行 开发员必须注意避免将这些 GUI 线程与较耗时间的计算工 作绑在一起 因为这些线程必须负责处理用户时间并重绘用户图形界面 换句话来说 一旦 GUI 线程处于繁忙 整个程序看起来就象无响应状态 Swing 线程通过调用合适方法 通 知那些 Swing callback 例如 Mouse Listener 和 Action L
21、istener 这种方法意味着 listener 无论要做多少事情 都应当利用 listener callback 方法产生其他线程来完成此项工 作 目的便在于让 listener callback 更快速返回 从而允许 Swing 线程响应其他事件 如果一个 Swing 线程不能够同步运行 响应事件并重绘输出 那怎么能够让其他的线程安 全地修改 Swing 的状态 正如上面提到的 Swing callback 在 Swing 线程中运行 因此 他们能修改 Swing 数据并绘到屏幕上 但是如果不是 Swing callback 产生的变化该怎么办呢 使用一个非 Swing 线程来修改 Swing 数据是不安全的 Swing 提供了两个方法来解决这个问题 invokeLater( 和 invokeAndWait( 为了修改 S
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025届寻甸回族彝族自治县三年级数学第一学期期末监测试题含解析
- 工程经济管理原则试题及答案
- 市政工程市场分析试题及答案
- 秋冬季消防安全教育
- 商业合作伙伴关系协议说明
- 买卖手购房合同书
- 项目监管体系的构建试题及答案
- 酒店旅游行业在线预订系统优化方案
- 农学作物病虫害防治知识卷
- 学历认证书教育背景证明(8篇)
- 线上陪玩店合同协议
- 蓉城小史官考试试题及答案
- GB/T 196-2025普通螺纹基本尺寸
- 中美关税贸易战
- 土地房屋测绘项目投标方案技术标
- 中华人民共和国农村集体经济组织法
- 中华传统文化之文学瑰宝学习通超星期末考试答案章节答案2024年
- 2023中华护理学会团体标准-注射相关感染预防与控制
- 在线考试系统的设计与实现论文
- 吊顶检验报告(共5页)
- 供水公司组织机构配置
评论
0/150
提交评论