当前位置: 首页 > 图灵资讯 > 技术篇> 编写高效的线程安全类

编写高效的线程安全类

来源:图灵教育
时间:2024-02-25 14:01:50
语言级支持锁定对象和线程之间的信件使编写线程安全变得简单。本文使用了一个简单的编程示例来解释高效的线程安全类别的开发是多么有效和直观。

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 程序中的多线程编程相当简单。然而,仍然很难使线程安全类高效。为了提高性能,您必须事先考虑并仔细使用锁定功能。