在语言级支持锁定对象和线程间发信使编写线程安全类变得简单
本文使用简单的编程示例来说明开发高效的线程安全类是多么有效而直观
Java 编程语言为编写多线程应用程序提供强大的语言支持但是编写有用的没有错误的多线程程序仍然比较困难本文试图概述几种方法程序员可用这几种方法来创建高效的线程安全类
1.并发性
只有当要解决的问题需要一定程度的并发性时程序员才会从多线程应用程序中受益例如如果打印队列应用程序仅支持一台打印机和一台客户机则不应该将它编写为多线程一般说来包含并发性的编码问题通常都包含一些可以并发执行的操作同时也包含一些不可并发执行的操作例如为多个客户机和一个打印机提供服务的打印队列可以支持对打印的并发请求但向打印机的输出必须是串行形式的多线程实现还可以改善交互式应用程序的响应时间
2.Synchronized 关键字
虽然多线程应用程序中的大多数操作都可以并行进行但也有某些操作(如更新全局标志或处理共享文件)不能并行进行在这些情况下必须获得一个锁来防止其他线程在执行此操作的线程完成之前访问同一个方法在 Java 程序中这个锁是通过 synchronized 关键字提供的清单 说明了它的用法
清单 使用 synchronized 关键字来获取锁
public class MaxScore {
int max;
public MaxScore() {
max = ;
}
public synchronized void currentScore(int s) {
if(s> max) {
max = s;
}
}
public int max() {
return max;
}
}
这里两个线程不能同时调用 currentScore() 方法当一个线程工作时另一个线程必须阻塞但是可以有任意数量的线程同时通过 max() 方法访问最大值因为 max() 不是同步方法因此它与锁定无关
试考虑在 MaxScore 类中添加另一个方法的影响该方法的实现如清单 所示
清单 添加另一个方法
public synchronized void reset() {
max = ;
}
这个方法(当被访问时)不仅将阻塞 reset() 方法的其他调用而且也将阻塞 MaxScore 类的同一个实例中的 currentScore() 方法因为这两个方法都访问同一个锁如果两个方法必须不彼此阻塞则程序员必须在更低的级别使用同步清单 是另一种情况其中两个同步的方法可能需要彼此独立
清单 两个独立的同步方法
import javautil*;
public class Jury {
Vector members;
Vector alternates;
public Jury() {
members = new Vector( );
alternates = new Vector( );
}
public synchronized void addMember(String name) {
membersadd(name);
}
public synchronized void addAlt(String name) {
alternatesadd(name);
}
public synchronized Vector all() {
Vector retval = new Vector(members);
retvaladdAll(alternates);
return retval;
}
}
此处两个不同的线程可以将 members 和 alternates 添加到 Jury 对象中请记住synchronized 关键字既可用于方法更一般地也可用于任何代码块清单 中的两段代码是等效的
清单 等效的代码
synchronized void f() { void f() {
// 执行某些操作
synchronized(this) {
} // 执行某些操作
}
}
所以为了确保 addMember() 和 addAlt() 方法不彼此阻塞可按清单 所示重写 Jury 类
清单 重写后的 Jury 类
import javautil*;
public class Jury {
Vector members;
Vector alternates;
public Jury() {
members = new Vector( );
alternates = new Vector( );
}
public void addMember(String name) {
synchronized(members) {
membersadd(name);
}
}
public void addAlt(String name) {
synchronized(alternates) {
alternatesadd(name);
}
}
public Vector all() {
Vector retval;
synchronized(members) {
retval = new Vector(members);
}
synchronized(alternates) {
retvaladdAll(alternates);
}
return retval;
}
}
请注意我们还必须修改 all() 方法因为对 Jury 对象同步已没有意义在改写后的版本中addMember()addAlt() 和 all() 方法只访问与 members 和 alternates 对象相关的锁因此锁定 Jury 对象毫无用处另请注意all() 方法本来可以写为清单 所示的形式
清单 将 members 和 alternates 用作同步的对象
public Vector all() {
synchronized(members) {
synchronized(alternates) {
Vector retval;
retval = new Vector(members);
retvaladdAll(alternates);
}
}
return retval;
}
但是因为我们早在需要之前就获得 members 和 alternates 的锁所以这效率不高清单 中的改写形式是一个较好的示例因为它只在最短的时间内持有锁并且每次只获得一个锁这样就完全避免了当以后增加代码时可能产生的潜在死锁问题
3.同步方法的分解
正如在前面看到的那样同步方法获取对象的一个锁如果该方法由不同的线程频繁调用则此方法将成为瓶颈因为它会对并行性造成限制从而会对效率造成限制这样作为一个一般的原则应该尽可能地少用同步方法尽管有这个原则但有时一个方法可能需要完成需要锁定一个对象几项任务同时还要完成相当耗时的其他任务在这些情况下可使用一个动态的锁定释放锁定释放方法例如清单 和清单 显示了可按这种方式变换的代码
清单 最初的低效率代码
public synchonized void doWork() {
unsafe();
write_file();
unsafe();
}
清单 重写后效率较高的代码
public void doWork() {
synchonized(this) {
unsafe();
}
write_file();
synchonized(this) {
unsafe();
}
}
清单 和清单 假定第一个和第三个方法需要对象被锁定而更耗时的 write_file() 方法不需要对象被锁定如您所见重写此方法以后对此对象的锁在第一个方法完成以后被释放然后在第三个方法需要时重新获得这样当 write_file() 方法执行时等待此对象的锁的任何其他方法仍然可以运行将同步方法分解为这种混合代码可以明显改善性能但是您需要注意不要在这种代码中引入逻辑错误
4.嵌套类
内部类在 Java 程序中实现了一个令人关注的概念它允许将整个类嵌套在另一个类中嵌套类作为包含它的类的一个成员变量如果定期被调用的的一个特定方法需要一个类就可以构造一个嵌套类此嵌套类的唯一任务就是定期调用所需的方法这消除了对程序的其他部分的相依性并使代码进一步模块化清单 一个图形时钟的基础使用了内部类
清单 图形时钟示例
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();
rstart();
}
private void repaint() {
// 获取时间的系统调用
// 重绘时钟指针
}
}
清单 中的代码示例不靠任何其他代码来调用 repaint() 方法这样将一个时钟并入一个较大的用户界面就相当简单
5.事件驱动处理
当应用程序需要对事件或条件(内部的和外部的)作出反映时有两种方法或用来设计系统
在第一种方法(称为轮询)中系统定期确定这一状态并据此作出反映这种方法(虽然简单)也效率不高因为您始终无法预知何时需要调用它
第二种方法(称为事件驱动处理)效率较高但实现起来也较为复杂在事件驱动处理的情况下需要一种发信机制来控制某一特定线程何时应该运行在 Java 程序中您可以使用 wait()notify() 和 notifyAll() 方法向线程发送信号这些方法允许线程在一个对象上阻塞直到所需的条件得到满足为止然后再次开始运行这种设计减少了 CPU 占用因为线程在阻塞时不消耗执行时间并且可在 notify() 方法被调用时立即唤醒与轮询相比事件驱动方法可以提供更短的响应时间
6.创建高效的线程安全类的步骤
编写线程安全类的最简单的方法是用 synchronized 声明每个方法虽然这种方案可以消除数据损坏但它同时也会消除您预期从多线程获得的任何收益这样您就需要分析并确保在 synchronized 块内部仅占用最少的执行时间您必须格外关注访问缓慢资源(文件目录网络套接字和数据库)的方法这些方法可能降低您的程序的效率尽量将对这类资源的访问放在一个单独的线程中最好在任何 synchronized 代码之外
一个线程安全类的示例被设计为要处理的文件的中心储存库它与使用 getWork() 和 finishWork() 与 WorkTable 类对接的一组线程一起工作本例旨在让您体验一下全功能的线程安全类该类使用了 helper 线程和混合同步请注意继续添加要处理的新文件的Refresher helper 线程的用法本例没有调整到最佳性能很明显有许多地方可以改写以改善性能比如将 Refresher 线程改为使用 wait()/notify() 方法事件驱动的改写 populateTable() 方法以减少列出磁盘上的文件(这是高成本的操作)所产生的影响
7.小结
通过使用可用的全部语言支持Java 程序中的多线程编程相当简单但是使线程安全类具有较高的效率仍然比较困难为了改善性能您必须事先考虑并谨慎使用锁定功能