Java 编程语言为编写多线程应用程序提供强大的语言支持。但是,编写有用的、没有错误的多线程程序仍然比较困难。本文试图概述几种方法,程序员可用这几种方法来创建高效的线程安全类。
并发性
只有当要解决的问题需要一定程度的并发性时,程序员才会从多线程应用程序中受益。例如,如果打印队列应用程序仅支持一台打印机和一台客户机,则不应该将它编写为多线程的。一般说来,包含并发性的编码问题通常都包含一些可以并发执行的操作,同时也包含一些不可并发执行的操作。例如,为多个客户机和一个打印机提供服务的打印队列可以支持对打印的并发请求,但向打印机的输出必须是串行形式的。多线程实现还可以改善交互式应用程序的响应时间。
Synchronized 关键字
虽然多线程应用程序中的大多数操作都可以并行进行,但也有某些操作(如更新全局标志或处理共享文件)不能并行进行。在这些情况下,必须获得一个锁来防止其他线程在执行此操作的线程完成之前访问同一个方法。在 Java 程序中,这个锁是通过 synchronized 关键字提供的。清单 1 说明了它的用法。
清单 1. 使用 synchronized 关键字来获取锁
public class MaxScore { int max; public MaxScore() { max = 0; } public synchronized void currentScore(int s) { if(s> max) { max = s; } } public int max() { return max; } }
这里,两个线程不能同时调用 currentScore() 方法;当一个线程工作时,另一个线程必须阻塞。但是,可以有任意数量的线程同时通过 max() 方法访问最大值,因为 max() 不是同步方法,因此它与锁定无关。
试考虑在 MaxScore 类中添加另一个方法的影响,该方法的实现如清单 2 所示。
清单 2. 添加另一个方法
public synchronized void reset() { max = 0; }
这个方法(当被访问时)不仅将阻塞 reset() 方法的其他调用,而且也将阻塞 MaxScore 类的同一个实例中的 currentScore() 方法,因为这两个方法都访问同一个锁。如果两个方法必须不彼此阻塞,则程序员必须在更低的级别使用同步。清单 3 是另一种情况,其中两个同步的方法可能需要彼此独立。
清单 3. 两个独立的同步方法
import java.util.*; public class Jury { Vector members; Vector alternates; public Jury() { members = new Vector(12, 1); alternates = new Vector(12, 1); } public synchronized void addMember(String name) { members.add(name); } public synchronized void addAlt(String name) { alternates.add(name); } public synchronized Vector all() { Vector retval = new Vector(members); retval.addAll(alternates); return retval; } }
此处,两个不同的线程可以将 members 和 alternates 添加到 Jury 对象中。请记住, synchronized 关键字既可用于方法,更一般地,也可用于任何代码块。清单 4 中的两段代码是等效的。
清单 4. 等效的代码
synchronized void f() { } synchronized(this) { void f() { } }
所以,为了确保 addMember() 和 addAlt() 方法不彼此阻塞,可按清单 5 所示重写 Jury 类。
清单 5. 重写后的 Jury 类
import java.util.*; public class Jury { Vector members; Vector alternates; public Jury() { members = new Vector(12, 1); alternates = new Vector(12, 1); } public void addMember(String name) { synchronized(members) { members.add(name); } } public void addAlt(String name) { synchronized(alternates) { alternates.add(name); } } public Vector all() { Vector retval; synchronized(members) { retval = new Vector(members); } synchronized(alternates) { retval.addAll(alternates); } return retval; } }
请注意,我们还必须修改 all() 方法,因为对 Jury 对象同步已没有意义。在改写后的版本中,addMember()、addAlt() 和 all() 方法只访问与 members 和 alternates 对象相关的锁,因此锁定 Jury 对象毫无用处。另请注意,all() 方法本来可以写为清单 6 所示的形式。
清单 6. 将 members 和 alternates 用作同步的对象
public Vector all() { synchronized(members) { synchronized(alternates) { Vector retval; retval = new Vector(members); retval.addAll(alternates); } } return retval; }
但是,因为我们早在需要之前就获得 members 和 alternates 的锁,所以这效率不高。清单 5 中的改写形式是一个较好的示例,因为它只在最短的时间内持有锁,并且每次只获得一个锁。这样就完全避免了当以后增加代码时可能产生的潜在死锁问题。
同步方法的分解
正如在前面看到的那样,同步方法获取对象的一个锁。如果该方法由不同的线程频繁调用,则此方法将成为瓶颈,因为它会对并行性造成限制,从而会对效率造成限制。这样,作为一个一般的原则,应该尽可能地少用同步方法。尽管有这个原则,但有时一个方法可能需要完成需要锁定一个对象几项任务,同时还要完成相当耗时的其他任务。在这些情况下,可使用一个动态的“锁定-释放-锁定-释放”方法。例如,清单 7 和清单 8 显示了可按这种方式变换的代码。
清单 7. 最初的低效率代码
public synchonized void doWork() { unsafe1(); write_file(); unsafe2(); }
清单 8. 重写后效率较高的代码
public void doWork() { synchonized(this) { unsafe1(); } write_file(); synchonized(this) { unsafe2(); } }
清单 7 和清单 8 假定第一个和第三个方法需要对象被锁定,而更耗时的 write_file() 方法不需要对象被锁定。如您所见,重写此方法以后,对此对象的锁在第一个方法完成以后被释放,然后在第三个方法需要时重新获得。这样,当 write_file() 方法执行时,等待此对象的锁的任何其他方法仍然可以运行。将同步方法分解为这种混合代码可以明显改善性能。但是,您需要注意不要在这种代码中引入逻辑错误。
嵌套类
内部类在 Java 程序中实现了一个令人关注的概念,它允许将整个类嵌套在另一个类中。嵌套类作为包含它的类的一个成员变量。如果定期被调用的的一个特定方法需要一个类,就可以构造一个嵌套类,此嵌套类的唯一任务就是定期调用所需的方法。这消除了对程序的其他部分的相依性,并使代码进一步模块化。清单 9,一个图形时钟的基础,使用了内部类。
清单 9. 图形时钟示例
public class Clock { protected class Refresher extends Thread { int refreshTime; public Refresher(int x) { super("Refresher"); refreshTime = x; } public void run() { while(true) { try { sleep(refreshTime); } catch(Exception e) {} repaint(); } } } public Clock() { Refresher r = new Refresher(1000); r.start(); } private void repaint() { // 获取时间的系统调用 // 重绘时钟指针 } }
清单 9 中的代码示例不靠任何其他代码来调用 repaint() 方法。这样,将一个时钟并入一个较大的用户界面就相当简单。
事件驱动处理
当应用程序需要对事件或条件(内部的和外部的)作出反映时,有两种方法或用来设计系统。在第一种方法(称为轮询)中,系统定期确定这一状态并据此作出反映。这种方法(虽然简单)也效率不高,因为您始终无法预知何时需要调用它。
第二种方法(称为事件驱动处理)效率较高,但实现起来也较为复杂。在事件驱动处理的情况下,需要一种发信机制来控制某一特定线程何时应该运行。在 Java 程序中,您可以使用 wait()、notify() 和 notifyAll() 方法向线程发送信号。这些方法允许线程在一个对象上阻塞,直到所需的条件得到满足为止,然后再次开始运行。这种设计减少了 CPU 占用,因为线程在阻塞时不消耗执行时间,并且可在 notify() 方法被调用时立即唤醒。与轮询相比,事件驱动方法可以提供更短的响应时间。
创建高效的线程安全类的步骤
编写线程安全类的最简单的方法是用 synchronized 声明每个方法。虽然这种方案可以消除数据损坏,但它同时也会消除您预期从多线程获得的任何收益。这样,您就需要分析并确保在 synchronized 块内部仅占用最少的执行时间。您必须格外关注访问缓慢资源 ― 文件、目录、网络套接字和数据库 ― 的方法,这些方法可能降低您的程序的效率。尽量将对这类资源的访问放在一个单独的线程中,最好在任何 synchronized 代码之外。
一个线程安全类的示例 被设计为要处理的文件的中心储存库。它与使用 getWork() 和 finishWork() 与 WorkTable 类对接的一组线程一起工作。本例旨在让您体验一下全功能的线程安全类,该类使用了 helper 线程和混合同步。请注意继续添加要处理的新文件的Refresher helper 线程的用法。本例没有调整到最佳性能,很明显有许多地方可以改写以改善性能,比如将 Refresher 线程改为使用 wait()/notify() 方法事件驱动的,改写 populateTable() 方法以减少列出磁盘上的文件(这是高成本的操作)所产生的影响。
小结
通过使用可用的全部语言支持,Java 程序中的多线程编程相当简单。但是,使线程安全类具有较高的效率仍然比较困难。为了改善性能,您必须事先考虑并谨慎使用锁定功能。
相关推荐
线程安全容器的实现方法通常包括以下几种: 1. **锁机制**:最简单的方法是在需要同步的代码块周围使用`lock`关键字。这会导致执行线程等待其他线程释放锁,确保一次只有一个线程执行临界区。 2. **读写锁**:如...
在计算机编程领域,尤其是涉及到实时系统和并发编程时,线程锁和线程安全变量是至关重要的概念。LabWindows/CVI是一种流行的交互式C开发环境,特别适合于开发科学和工程应用。本实例将深入探讨如何在LabWindows/CVI...
4. **管程(Monitor)**:Java中的`synchronized`关键字和`wait()`, `notify()`, `notifyAll()`方法其实就是一个简单的管程实现,它提供了线程安全的共享数据访问。 5. **事件(Event)**:Java的`java.util....
在Java编程语言中,线程安全是多线程环境下程序正确性和稳定性的重要概念。线程安全测试类的设计是为了确保在并发环境中,多个线程访问共享资源时不会导致数据的不一致或异常行为。本测试主要关注`synchronized`...
要实现线程安全的双链表,我们有几种常见的策略: 1. **互斥锁(Mutex)**:每个操作(如插入、删除)都先获取锁,操作完成后释放锁。这样确保了同一时间只有一个线程能执行这些操作,从而避免了竞态条件。然而,这...
在实现线程安全的循环单链表时,还需要考虑以下几点: 1. **迭代器**:为了支持遍历链表,可以实现`Iterable`接口并提供一个线程安全的迭代器。迭代器的`hasNext()`和`next()`方法也需要使用`synchronized`关键字。 ...
在C#中,有几种方法可以实现线程安全: 1. **锁(Lock)**:使用`lock`关键字可以创建一个同步块,只允许一个线程进入。例如,当你需要读取或写入串口时,可以包裹相关代码在`lock`语句中,防止并发访问。 ```...
实现可重入性通常有以下几种方式: 1. **避免使用静态变量**:静态变量在函数退出后仍保留其值,这可能导致不同线程间的冲突。因此,可重入函数应避免使用静态局部变量。 2. **使用线程私有数据**:线程私有数据...
为了克服上述问题,可以采用以下几种解决方案: 1. **显式同步**:在迭代过程中通过同步代码块或同步方法的方式锁定整个 `Vector` 对象。 2. **使用迭代器**:使用 `Vector` 提供的迭代器进行遍历,这样可以在一定...
总的来说,编写线程代码需要深入理解多线程的概念,掌握各种创建和管理线程的方法,并确保线程安全和高效性。在实际项目中,结合使用`ExecutorService`和线程池通常是最佳实践,因为它们提供了更好的性能和资源管理...
Joshua Bloch在其著作《Effective Java》中提出了一种线程安全性等级的概念,将线程安全性分为以下几个等级: 1. **线程安全(Thread-Safe)**:即使没有外部同步机制,也可以在多线程环境下正确工作。 2. **可重入...
在处理多线程时,有几个关键概念和特性需要理解: - **线程同步**:为了避免线程间的冲突,需要同步对共享资源的访问。C#提供了`Mutex`, `Semaphore`, `Monitor`, `lock`等机制。 - **线程优先级**:每个线程都有...
针对上述问题,可以通过以下几种方式来确保Delphi多线程程序的安全性: 1. **使用临界区(Critical Section)**:临界区是一种轻量级的同步机制,用于保护那些一次只能被一个线程访问的共享资源。通过使用`...
为了帮助大家更好地理解和掌握 Java 多线程的知识,本文总结了 Java 多线程的几种实现方法,并对多线程的基本概念、状态以及状态之间的关系进行了详细的解释。 一、多线程的实现方法 Java 中有两种实现多线程的...
在Java中,我们通常通过以下几种方式来保证线程安全: 1. **同步机制**:包括`synchronized`关键字、`Lock`接口(如`ReentrantLock`)以及`java.util.concurrent.locks`包下的其他工具。它们可以限制对共享资源的...
线程在其生命周期中会经历几种不同的状态: 1. **新建态(Newborn)**:当通过`new`关键字创建线程但尚未调用`start()`方法时,线程处于新建状态。 2. **就绪状态(Runnable)**:调用`start()`方法后,线程变为...
VB多线程控件是开发者用来实现这一功能的一种工具,但正如标题和描述所指出的,虽然可以实现简单的多线程,但确实存在一些限制和挑战。 首先,我们要理解什么是线程。线程是程序执行的基本单元,每个线程都有自己的...
在Java编程语言中,实现多线程文件传输是一种优化程序性能、提高系统资源利用率的有效方法。多线程允许我们同时处理多个任务,这对于大文件传输或需要并行处理的场景尤其有用。本篇文章将深入探讨如何使用Java实现多...
线程的创建通常有以下几种方式: 1. 继承Thread类:创建一个新的类,该类继承Thread类并重写它的run()方法。然后创建该类的实例并调用其start()方法来启动线程。 2. 实现Runnable接口:创建一个实现了Runnable接口的...