`
kakaluyi
  • 浏览: 444977 次
  • 性别: Icon_minigender_1
  • 来自: 苏州
社区版块
存档分类
最新评论

编写高效的线程安全类

阅读更多
 
文档选项
将打印机的版面设置成横向打印模式

打印本页

将此页作为电子邮件发送

将此页作为电子邮件发送

<!----><!----><!---->

级别: 初级

Neel V. Kumar (neelvk@terway.com), 软件工程师, Terway.com

2000 年 4 月 01 日

在语言级支持锁定对象和线程间发信使编写线程安全类变得简单。本文使用简单的编程示例来说明开发高效的线程安全类是多么有效而直观。
<!----><!----><!---->

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() {              void f() {      
    // 执行某些操作                                              synchronized(this) {
}                                                    // 执行某些操作
                                            }
                                     }  

所以,为了确保 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 程序中的多线程编程相当简单。但是,使线程安全类具有较高的效率仍然比较困难。为了改善性能,您必须事先考虑并谨慎使用锁定功能。



参考资料



关于作者

 

Neel V. Kumar 是一位具有八年面向对象编程经验的软件工程师,所用的语言为 C++ 和 Java 编程语言。他出生在爱荷华州,目前住在加利福尼亚的 Menlo Park,刚刚涉足电信领域。他曾经为许多项目提供过咨询服务,并乐意与别人分享他的知识。可以通过 neelvk@terway.com 与他联系。

并发性
分享到:
评论

相关推荐

    C#编写多线程程序源码

    通过学习和理解这些概念,并结合提供的"C#编写多线程程序源码"实例,你可以更好地掌握C#中的多线程编程,从而编写出高效、稳定的应用程序。在实际开发中,还需要考虑线程安全、性能优化以及错误处理等细节。

    CVI 线程锁、线程安全变量实例

    理解并熟练运用这些概念和技术,可以帮助你在LabWindows/CVI环境中编写出高效且稳定的多线程应用程序,避免因数据不一致或竞态条件而导致的问题。在实践中,要注意合理分配资源,避免死锁,以及对线程性能的优化,...

    c#编写多线程

    .NET Framework提供了线程安全的数据结构,如ConcurrentQueue、ConcurrentStack和ConcurrentDictionary,它们在多线程环境中提供线程安全的添加、移除和访问操作。 总结,C#的多线程编程涵盖了许多方面,包括线程的...

    创建线程类轻松实现线程管理

    在编程领域,线程是操作系统分配CPU时间的基本单位,它...记住,多线程编程虽然强大,但也需谨慎处理线程安全问题,避免出现未预期的行为。通过熟练掌握线程类和线程管理,你可以编写出更加健壮、高效的Java应用程序。

    安全的编写多线程的_Java_应用程序

    然而,多线程编程同时也带来了一系列挑战,如线程安全问题、死锁等。本文将详细介绍如何安全地编写多线程Java应用程序,避免最常见的问题,并提供一些实用的解决方案。 #### 二、多线程基础 **1. 线程概念** - **...

    构建线程安全应用程序

    线程安全是指在多线程环境中,一个类或函数能够按照其规格说明正确执行,即使在各种线程调度或交错...通过使用线程安全的类库,遵循良好的并发编程实践,并进行彻底的测试,可以构建出既安全又高效的线程安全应用程序。

    java线程安全测试

    总之,理解Java内存模型和掌握线程安全策略对于开发高效、可靠的并发程序至关重要。开发者应始终关注线程安全,确保在多线程环境下代码的正确性和稳定性。在实际项目中,可以参考提供的"ThreadSaft"文件中的测试例子...

    Java编写线程动画

    Java编程中的线程动画是一种利用多线程技术在图形用户...通过上述知识,开发者可以编写出流畅、高效的Java线程动画,为用户提供更优质的交互体验。同时,不断学习和实践将帮助你更好地理解和掌握Java多线程编程的精髓。

    编写线程安全的JSP程序

    这种特性允许JSP能够高效地处理并发请求,但也引入了线程安全问题。 1. **线程安全问题**: - **实例变量**:在JSP中,实例变量是共享的,因此在多线程环境中,不同线程可能会同时访问并修改这些变量,导致数据不...

    Java多线程安全集合

    Java提供了一系列的线程安全集合类,它们是专门为多线程环境设计的。 首先,我们要了解什么是线程安全。线程安全是指一个类或者方法在多线程环境中被调用时,能够正确地处理并发访问,不会因为线程间的交互而产生...

    C#线程安全的事件类研究报告

    通过这份研究报告,你将能够深入理解C#中线程安全事件的实现,避免因并发操作引发的错误,从而编写出更稳定、高效的多线程应用程序。提供的"如何在一个类型中定义多个自定义的线程安全的事件.pdf"文件应该包含了更多...

    java线程安全总结.doc

    2. 使用线程安全的集合类,如`ConcurrentHashMap`、`CopyOnWriteArrayList`等。 3. 避免长时间持有锁,减少阻塞时间。 4. 优先使用更高粒度的同步,减少锁的使用范围。 5. 对于不可变对象,尽可能地使用它们,减少...

    C#编写多线程搜索引擎

    - **同步与通信**:为了避免线程间的冲突,可能使用锁(如`Monitor`,`Mutex`,`Semaphore`等)或者`Concurrent`集合来实现线程安全的数据访问。 - **异常处理**:确保每个线程都有适当的异常处理机制,防止程序崩溃...

    Action是否线程安全

    在Java中,理解和遵循多线程的最佳实践对于编写高效且可靠的并发代码至关重要。对于`javax.swing.ActionEvent`,理解Swing的事件模型和EDT的工作原理是保证线程安全的关键。而对于自定义的`Action`接口,开发者需要...

    Qt两种方法实现多线程并安全结束线程及QMutex加锁Qt单例化实现

    本主题将详细探讨如何使用Qt的QThread类来创建和管理线程,并且安全地结束线程,同时结合QMutex进行线程同步,以及如何实现Qt的单例模式。 1. **QThread的使用**: - **创建线程**: 在Qt中,创建线程通常是通过...

    Windows线程封装类(VC++)

    2. **线程同步**:通过事件对象、互斥量、信号量或临界区等机制确保线程安全,防止数据竞争。 3. **线程通信**:使用队列、共享内存、管道或消息队列进行线程间的通信。 4. **线程退出**:设置线程退出状态,使用`...

    线程安全Vector

    ### 线程安全Vector详解 #### 一、线程安全的基本概念 在软件开发中,尤其是在并发编程领域,线程安全是一个极为重要的概念。...理解不同集合类的特点及其适用场景对于编写高效、可靠的多线程应用至关重要。

    Java并发中的线程安全性

    设计线程安全类的原则 为了设计出线程安全的类,可以遵循以下原则: 1. **避免使用共享可变状态**:通过减少类内部共享的状态数量,可以降低线程安全的复杂度。 2. **利用不可变对象**:使用不可变对象可以自然地...

Global site tag (gtag.js) - Google Analytics