Java Thread API 允许程序员编写具有多处理机制优点的应用程序,在后台处理任务的同时保持用户所需的交互感。Alex
Roetter 介绍了 Java Thread API,并概述多线程可能引起的问题以及常见问题的解决方案。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
几乎所有使用 AWT 或 Swing
编写的画图程序都需要多线程。但多线程程序会造成许多困难,刚开始编程的开发者常常会发现他们被一些问题所折磨,例如不正确的程序行为或死锁。
在本文中,我们将探讨使用多线程时遇到的问题,并提出那些常见陷阱的解决方案。
线程是什么?
一个程序或进程能够包含多个线程,这些线程可以根据程序的代码执行相应的指令。多线程看上去似乎在并行执行它们各自的工作,就像在一台计算机上运行着多个处理机一样。在多处理机计算机上实现多线程时,它们确实
可以并行工作。和进程不同的是,线程共享地址空间。?簿褪撬担喔鱿叱棠芄欢列聪嗤谋淞炕蚴萁峁埂?
编写多线程程序时,你必须注意每个线程是否干扰了其他线程的工作。可以将程序看作一个办公室,如果不需要共享办公室资源或与其他人交流,所有职员就会独立并行地工作。某个职员若要和其他人交谈,当且仅当该职员在“听”且他们两说同样的语言。此外,只有在复印机空闲且处?诳捎米刺挥薪鐾瓿梢话氲母从」ぷ鳎挥兄秸抛枞任侍猓┦保霸辈拍芄皇褂盟T谡馄恼轮心憬吹剑?Java
程序中互相协作的线程就好像是在一个组织良好的机构中工作的职员。
在多线程程序中,线程可以从准备就绪队列中得到,并在可获得的系统 CPU
上运行。操作系统可以将线程从处理器移到准备就绪队列或阻塞队列中,这种情况可以认为是处理器“挂起”了该线程。同样,Java 虚拟机 (JVM)
也可以控制线程的移动――在协作或抢先模型中――从准备就绪队列中将进程移到处理器中,于是该线程就可以开始执行它的程序代码。
协作式线程
模型允许线程自己决定什么时候放弃处理器来等待其他的线程。程序开发员可以精确地决定某个线程何时会被其他线程挂起,允许它们与对方有效地合作。缺点在于某些恶意或是写得不好的线程会消耗所有可获得的
CPU 时间,导致其他线程“饥饿”。
在 抢占式线程
模型中,操作系统可以在任何时候打断线程。通常会在它运行了一段时间(就是所谓的一个时间片)后才打断它。这样的结果自然是没有线程能够不公平地长时间霸占处理器。然而,随时可能打断线程就会给程序开发员带来其他麻烦。同样使用办公室的例子,假设某个职员抢在另一人前使用复印机,但打印工作在未完成的时候离开了,另一人接着使用复印机时,该复印机上可能就还有先前那名职员留下来的资料。抢占式线程模型要求线程正确共享资源,协作式模型却要求线程共享执行时间。由于
JVM 规范并没有特别规定线程模型,Java
开发员必须编写可在两种模型上正确运行的程序。在了解线程以及线程间通讯的一些方面之后,我们可以看到如何为这两种模型设计程序。
线程和 Java 语言
为了使用 Java 语言创建线程,你可以生成一个 Thread
类(或其子类)的对象,并给这个对象发送 start()
消息。(程序可以向任何一个派生自
Runnable
接口的类对象发送 start()
消息。)每个线程动作的定义包含在该线程对象的 run()
方法中。run 方法就相当于传统程序中的 main()
方法;线程会持续运行,直到 run()
返回为止,此时该线程便死了。
上锁
大多数应用程序要求线程互相通信来同步它们的动作。在 Java
程序中最简单实现同步的方法就是上锁。为了防止同时访问共享资源,线程在使用资源的前后可以给该资源上锁和开锁。假想给复印机上锁,任一时刻只有一个职员拥有钥匙。若没有钥匙就不能使用复印机。给共享变量上锁?褪沟?Java
线程能够快速方便地通信和同步。某个线程若给一个对象上了锁,就可以知道没有其他线程能够访问该对象。即使在抢占式模型中,其他线程也不能够访问此对象,直到上锁的线程被唤醒、完成工作并开锁。那些试图访问一个上锁对象的线程通常会进入睡眠状态,直到上锁的线?炭R坏┧淮蚩庑┧呓叹突岜换叫巡⒁频阶急妇托鞫恿兄小?/P>
在 Java 编程中,所有的对象都有锁。线程可以使用 synchronized
关键字来获得锁。在任一时刻对于给定的类的实例,方法或同步的代码块只能被一个线程执行。这是因为代码在执行之前要求获得对象的锁。继续我们关于复印机的比喻,为了?苊飧从〕逋唬颐强梢约虻サ囟愿从∽试词敌型健H缤铝械拇肜樱我皇笨讨辉市硪晃恢霸笔褂酶从∽试础Mü褂梅椒ǎㄔ?Copier
对象中)来修改复印机状态。这个方法就是同步方法。只有一个线程能够执行一个
对象中同步代码,因此那些需要使用 Copier
对象的职员就必须排队等候。
class CopyMachine {
public synchronized void makeCopies(Document d, int nCopies) {
//only one thread executes this at a time
}
public void loadPaper() {
//multiple threads could access this at 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;
Object xlock = new Object(), ylock = new Object();
public void foo() {
synchronized(xlock) {
//access x here
}
//do something here - but don't use shared resources
synchronized(ylock) {
//access y here
}
}
public void bar() {
synchronized(this) {
//access both x and y here
}
//do something here - but don't use shared resources
}
}
|
若为了在方法级上同步,不能将整个方法声明为 synchronized
关键字。它们使用的是成员锁,而不是 synchronized 方法能够获得的对象级锁。
信号量
通常情况下,可能有多个线程需要访问数目很少的资源。假想在服务器上运行着若干个回答客户端请求的线程。这些线程需要连接到同一数据库,但任一时刻只能获得一定数目的数据库连接。你要怎样才能够有效地将这些固定数目的数据库连接分配给大量的线程?一种控制访问一组资源?姆椒ǎǔ思虻サ厣纤猓褪鞘褂弥谒苤男藕帕考剖?(counting
semaphore)。
信号量计数将一组可获得资源的管理封装起来。信号量是在简单上锁的基础上实现的,相当于能令线程安全执行,并初始化为可用资源个数的计数器。例如我们可以将一个信号量初始化为可获?玫氖菘饬痈鍪R坏┠掣鱿叱袒竦昧诵藕帕浚苫竦玫氖菘饬邮跻弧O叱滔耐曜试床⑹头鸥米试词保剖骶突峒右弧5毙藕帕靠刂频乃凶试炊家驯徽加檬保粲邢叱淌酝挤梦蚀诵藕帕浚蚧峤胱枞刺钡接锌捎米试幢皇头拧?
信号量最常见的用法是解决“消费者-生产者问题”。当一个线程进行工作时,若另外一个线程访问同一共享变量,就可能产生此问题。消费者线程只能在生产者线程完成生产后才能够访问数据。使用信号量来解决这个问题,就需要创建一个初始化为零的信号量,从而让消费者线程访问?诵藕帕渴狈⑸枞C康蓖瓿傻ノ还ぷ魇保呦叱叹突嵯蚋眯藕帕糠⑿藕牛ㄊ头抛试矗C康毕颜呦叱滔蚜说ノ簧峁⑿枰碌氖莸ピ保突崾酝荚俅位袢⌒藕帕俊R虼诵藕帕康闹稻妥苁堑扔谏瓯峡晒┫训氖莸ピU庵址椒ū炔捎孟颜呦叱滩煌<觳槭欠裼锌捎檬莸ピ姆椒ㄒ咝У枚唷R蛭颜呦叱绦牙春螅热裘挥姓业娇捎玫氖莸ピ突嵩俣冉胨咦刺庋牟僮飨低晨欠浅0汗蟮摹?/P>
尽管信号量并未直接被 Java 语言所支持,却很容易在给对象上锁的基础上实现。一个简单的实现方法如下所示:
class Semaphore {
private int count;
public Semaphore(int n) {
this.count = n;
}
public synchronized void acquire() {
while(count == 0) {
try {
wait();
} catch (InterruptedException e) {
//keep trying
}
}
count--;
}
public synchronized void release() {
count++;
notify(); //alert a thread that's blocking on this semaphore
}
}
|
常见的上锁问题
不幸的是,使用上锁会带来其他问题。让我们来看一些常见问题以及相应的解决方法:
![](http://www.ibm.com/i/v14/rules/blue_rule.gif)
|
![](http://www.ibm.com/i/c.gif)
|
为不同的线程模型进行设计
判断是抢占式还是协作式的线程模型,取决于虚拟机的实现者,并根据各种实现而不同。因此,Java 开发员必须编写那些能够在两种模型上工作的程序。
正如前面所提到的,在抢占式模型中线程可以在代码的任何一个部分的中间被打断,除非那是一个原子操作代码块。原子操作代码块中的代码段一旦开始执行,就要在该线程被换出处理器之前执行完毕。在
Java 编程中,分配一个小于 32 位的变量空间是一种原子操作,而此外象 double
和 long
这两个 64
位数据类型的分配就不是原子的。使用锁来正确同步共享资源的访问,就足以保证一个多线程程序在抢占式模型下正确工作。
而在协作式模型中,是否能保证线程正常放弃处理器,不掠夺其他线程的执行时间,则完全取决于程序员。调用 yield()
方法能够将当前的线程从处理器中移出到准备就绪队列中。另一个方法则是调用 sleep()
方法,使线程放弃处理器,并且在 sleep 方法中指定的时间间隔内睡眠。
正如你所想的那样,将这些方法随意放在代码的某个地方,并不能够保证正常工作。如果线程正拥有一个锁(因为它在一个同步方法或代码块中),则当它调用
yield()
时不能够释放这个锁。这就意味着即使这个线程已经被挂起,等待这个锁释放的其他线程依然不能继续运行。为了缓解这个问题,最好不在同步方法中调用 yield
方法。将那些需要同步的代码包在一个同步块中,里面不含有非同步的方法,并且在这些同步代码块之外才调用
yield
。
另外一个解决方法则是调用 wait()
方法,使处理器放弃它当前拥有的对象的锁。如果对象在方法级别上使同步的,这种方法能够很好的工作。因为它仅仅使用了一个锁。如果它使用 fine-grained
锁,则 wait()
将无法放弃这些锁。此外,一个因为调用 wait()
方法而阻塞的线程,只有当其他线程调用 notifyAll()
时才会被唤醒。
线程和 AWT/Swing
在那些使用 Swing 和/或 AWT 包创建 GUI (用户图形界面)的 Java 程序中,AWT
事件句柄在它自己的线程中运行。开发员必须注意避免将这些 GUI
线程与较耗时间的计算工作绑在一起,因为这些线程必须负责处理用户时间并重绘用户图形界面。换句话来说,一旦 GUI
线程处于繁忙?龀绦蚩雌鹄淳拖笪尴煊ψ刺wing 线程通过调用合适方法,通知那些 Swing callback (例如 Mouse Listener 和
Action Listener )。 这种方法意味着 listener 无论要做多少事情,都应当利用 listener callback
方法产生其他线程来完成此项工作。目的便在于让 listener callback 更快速返回,从而允许 Swing 线程响应其他事件。
如果一个 Swing 线程不能够同步运行、响应事件并重绘输出,那怎么能够让其他的线程安全地修改 Swing 的状态?正如上面提到的,Swing
callback 在 Swing 线程中运行。因此他们能修改 Swing 数据并绘到屏幕上。
但是如果不是 Swing callback 产生的变化该怎么办呢?使用一个非 Swing 线程来修改 Swing 数据是不安全的。Swing
提供了两个方法来解决这个问题: invokeLater()
和 invokeAndWait()
。为了?薷?Swing 状态,只要简单地调用其中一个方法,让
Runnable
的对象来做这些工作。因为 Runnable
对象通常就是它们自身的线程,你可能会认为这些对象会作为线程来执行。但那样做其实也是不安全的。事实上,Swing 会将这些对象放到队列中,并在将来某个时刻执行它的
run 方法。这样才能够安全修改 Swing 状态。
分享到:
相关推荐
### 安全编写多线程Java应用程序的关键知识点 #### 一、引言 在现代软件开发中,多线程编程已成为提升程序性能和响应性的关键手段之一。Java作为一种广泛使用的编程语言,提供了丰富的多线程支持。然而,多线程...
总之,编写多线程Java应用程序需要深入理解线程的概念和Java提供的并发工具。开发者必须谨慎处理线程间的交互,避免数据不一致性、竞态条件和死锁。通过合理地使用同步机制和并发工具,可以创建出高效且可靠的多线程...
这个“java多线程控制的赛跑程序”是一个示例,展示了如何利用多线程来模拟一场赛跑比赛。在这个程序中,每个参赛者(线程)都有自己的运行逻辑,通过线程的并发执行来模拟实际的赛跑过程。接下来,我们将深入探讨...
在Java编程中,多线程是一项关键特性,它允许程序同时执行多个任务,极大地提高了效率。本实验"java多线程之赛马...通过实践,开发者可以更好地掌握并发编程的技巧,这对于编写高效、响应迅速的Java应用程序至关重要。
在“java多线程控制小球程序”这个项目中,我们看到一个具体的应用场景:多个小球在一个框内不断弹跳,并且这些小球可能在不同的时间发射。这样的设计可以模拟现实世界的物理现象,例如弹珠台或者粒子碰撞,同时也是...
在“JAVA编写的多线程小弹球测试”项目中,开发者利用Java语言创建了一个生动有趣的多线程应用,即一个模拟小弹球运动的程序。这个程序的特点是弹球会随机出现、随机选择颜色,并且在碰到边界时能自动反弹,充分展示...
通过JAVA运用多线程控制球的运动,通过窗口中的滑条,对球的大小和颜色进行选择后,随机从窗口左右两方择一进入,小球在遇到障碍或边界后会折回。
本示例可能是一个Applet,Applet是Java小程序,它可以在Web浏览器中运行,展示了如何在Java环境中应用多线程。 首先,我们要理解线程的基本概念。线程是程序执行的最小单元,每个线程都有自己的程序计数器、虚拟机...
在本实验中,我们主要探讨了Java多线程的同步机制以及其在并发编程中的应用。实验目的是理解和掌握并行/并发、同步/异步的概念,并通过实现一个模拟银行账户的程序来具体应用这些概念。 首先,理解并行/并发是关键...
通过分析并实践`threadTest`案例,我们可以深入理解Java多线程的原理和使用技巧,为编写高效并发程序打下坚实基础。同时,也要注意多线程编程中的死锁、活锁和饥饿等问题,合理设计线程间的交互,避免出现不可预期的...
本文将基于一个具体的Java多线程操作数据库的应用程序,深入探讨其背后的原理、实现细节以及潜在的挑战。 #### 核心知识点: 1. **多线程基础**:多线程是Java编程中的一个重要概念,允许程序同时执行多个任务。在...
Java多线程程序设计是Java开发中的重要领域,它允许应用程序同时执行多个任务,从而提高系统资源的利用率和程序的响应速度。在Java中,多线程主要通过两种方式实现:继承Thread类和实现Runnable接口。 一、创建线程...
### 如何使用Java编写多线程程序 #### 一、简介 ##### 1.1 什么是线程? 在深入探讨Java中的多线程编程之前,我们需要先了解什么是线程。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程...
标题所涉及的知识点为“Java多线程的聊天室程序”,...通过动手编写一个完整的聊天室程序,不仅可以加深对Java多线程和Socket编程的理解,还能提高解决实际问题的能力,为后续更深入地学习其他高级话题打下良好的基础。
在本实验中,我们要求学生编写一个 Java 应用程序,在主线程中再创建 2 个线程,要求线程经历 4 种状态:新建、运行、中断和死亡。学生需要使用 Thread 类中的 start() 方法来启动线程,并使用 run() 方法来实现线程...
总的来说,"java多线程+Socket+Swing做的局域网聊天程序"是一个综合性的项目,涵盖了Java基础、网络编程以及GUI设计等多个方面,对于学习和理解Java应用开发具有很高的实践价值。通过这样的项目,开发者可以提升对...
总之,Java的多线程和并发编程是一个复杂而重要的主题,它涉及到操作系统原理、JVM行为、线程管理、同步机制等多个方面,熟练掌握这些知识对于开发高效、可靠的Java应用程序至关重要。通过理解线程的工作原理和使用...
在Java编程中,多线程是一项关键特性,它允许程序同时执行多个独立的代码段,提高了应用程序的效率和响应性。下面将详细讲解Java多线程的相关概念、创建线程的方式以及线程同步和调度。 5.1 相关概念: 1. **程序**...
标题中的“多线程 小球 运行程序(eclipse工程可导入)”表明这是一个与多线程编程相关的项目,可能是用Java语言实现的,因为Eclipse是Java开发的常用集成开发环境。这个程序可能设计了一个模拟小球运动的场景,通过多...