Java理论与实践: 流行的原子

十五年前,多处理器系统是高度专用系统,要花费数十万美元(大多数具有 两个到四个处理器)。现在,多处理器系统很便宜,而且数量很多,几乎每个主 要微处理器都内置了多处理支持,其中许多系统支持数十个或数百个处理器。

要使用多处理器系统的功能,通常需要使用多线程构造应用程序。但是正如 任何编写并发应用程序的人可以告诉你的那样,要获得好的硬件利用率,只是简 单地在多个线程中分割工作是不够的,还必须确保线程确实大部分时间都在工作 ,而不是在等待更多的工作,或等待锁定共享数据结构。

问题:线程之间的协调

如果线程之间 不需要协调,那么几乎没有任务可以真正地并行。以线程池为 例,其中执行的任务通常相互独立。如果线程池利用公共工作队列,则从工作队 列中删除元素或向工作队列添加元素的过程必须是线程安全的,并且这意味着要 协调对头、尾或节点间链接指针所进行的访问。正是这种协调导致了所有问题。

标准方法:锁定

在 Java 语言中,协调对共享字段的访问的传统方法是使用同步,确保完成 对共享字段的所有访问,同时具有适当的锁定。通过同步,可以确定(假设类编 写正确)具有保护一组给定变量的锁定的所有线程都将拥有对这些变量的独占访 问权,并且以后其他线程获得该锁定时,将可以看到对这些变量进行的更改。弊 端是如果锁定竞争太厉害(线程常常在其他线程具有锁定时要求获得该锁定), 会损害吞吐量,因为竞争的同步非常昂贵。(Public Service Announcement: 对于现代 JVM 而言,无竞争的同步现在非常便宜。

基于锁定的算法的另一个问题是:如果延迟具有锁定的线程(因为页面错误 、计划延迟或其他意料之外的延迟),则 没有要求获得该锁定的线程可以继续 运行。

还可以使用可变变量来以比同步更低的成本存储共享变量,但它们有局限性 。虽然可以保证其他变量可以立即看到对可变变量的写入,但无法呈现原子操作 的读-修改-写顺序,这意味着(比如说)可变变量无法用来可靠地实现互斥(互 斥锁定)或计数器。

使用锁定实现计数器和互斥

假如开发线程安全的计数器类,那么这将暴露 get()、 increment() 和 decrement() 操作。清单 1 显示了如何使用锁定(同步)实现该类的例子。注 意所有方法,甚至需要同步 get(),使类成为线程安全的类,从而确保没有任何 更新信息丢失,所有线程都看到计数器的最新值。

清单 1. 同步的计数器类

public class SynchronizedCounter {
   private int value;
   public synchronized int getValue() { return value; }
   public synchronized int increment() { return ++value; }
   public synchronized int decrement() { return --value; }
}

increment() 和 decrement() 操作是原子的读-修改-写操作,为了安全实现 计数器,必须使用当前值,并为其添加一个值,或写出新值,所有这些均视为一 项操作,其他线程不能打断它。否则,如果两个线程试图同时执行增加,操作的 不幸交叉将导致计数器只被实现了一次,而不是被实现两次。(注意,通过使值 实例变量成为可变变量并不能可靠地完成这项操作。)

许多并发算法中都显示了原子的读-修改-写组合。清单 2 中的代码实现了简 单的互斥, acquire() 方法也是原子的读-修改-写操作。要获得互斥,必须确 保没有其他人具有该互斥( curOwner = Thread.currentThread()),然后记录 您拥有该互斥的事实( curOwner = Thread.currentThread()),所有这些使其 他线程不可能在中间出现以及修改 curOwner field。

清单 2. 同步的互斥类

public class SynchronizedMutex {
   private Thread curOwner = null;
   public synchronized void acquire() throws InterruptedException {
     if (Thread.interrupted()) throw new InterruptedException ();
     while (curOwner != null)
       wait();
     curOwner = Thread.currentThread();
   }
   public synchronized void release() {
     if (curOwner == Thread.currentThread()) {
       curOwner = null;
       notify();
     } else
       throw new IllegalStateException("not owner of mutex");
   }
}

清单 1 中的计数器类可以可靠地工作,在竞争很小或没有竞争时都可以很好 地执行。然而,在竞争激烈时,这将大大损害性能,因为 JVM 用了更多的时间 来调度线程,管理竞争和等待线程队列,而实际工作(如增加计数器)的时间却 很少。您可以回想 上月专栏中的图,该图显示了一旦多个线程使用同步竞争一 个内置监视器,吞吐量将如何大幅度下降。虽然该专栏说明了新的 ReentrantLock 类如何可以更可伸缩地替代同步,但是对于一些问题,还有更好 的解决方法。

时间: 2024-11-05 15:12:12

Java理论与实践: 流行的原子的相关文章

Java理论与实践专题

Java理论与实践: JDK 5.0中更灵活.更具可伸缩性的锁定机制 Java理论和实践: 一个有缺陷的微基准的剖析 Java理论和实践: 理解JTS ― 平衡安全性和性能 Java理论和实践: 理解JTS ― 幕后魔术 Java理论和实践: 安全构造技术 Java理论与实践: 平衡测试,第3部分:用方面检验设计约束 Java理论与实践:平衡测试,第2部分:编写和优化bug检测器 Java理论与实践:平衡测试,第1部分:不要仅编写测试,还要编写bu Java理论与实践: 您的小数点到哪里去了?

Java 理论与实践: 非阻塞算法简介

[本文转载自Java 理论与实践: 非阻塞算法简介]Java 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能.非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 -- 例如比较和交换.非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞吐率,对生存问题(例如死锁和优先级反转)也能提供更好的防御.在这期的 Java 理论与实践 中,并发性大师 Brian Goet

Java理论与实践: 构建一个更好的HashMap

ConcurrentHashMap 是 Doug Lea的 util.concurrent 包的一部分,它提供 比Hashtable 或者 synchronizedMap 更高程度的并发性.而且,对于大多数成 功的 get() 操作它会设法避免完全锁定,其结果就是使得并发应用程序有着非 常好的吞吐量.这个月,BrianGoetz 仔细分析了 ConcurrentHashMap的代码, 并探讨 Doug Lea 是如何在不损失线程安全的情况下取得这么骄人成绩的. 在7月份的那期 Java理论与实践

Java理论与实践: 修复Java内存模型,第1部分

活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型 (Java Memory Model, JMM)的公开建议.原始 JMM 中有几个严重缺陷,这导 致了一些难度高得惊人的概念语义,这些概念原来被认为很简单,如 volatile .final 以及 synchronized.在这一期的 Java 理论与实践 中,Brian Goetz 展示了如何加强 volatile 和 final 的语义,以修复 JMM.这些更改有些已经 集成在 JDK 1.4 中:而另一些将会包含

Java 理论与实践:变还是不变?

不变对象具有许多能更方便地使用它们的特性,包括不严格的同步需求和不必考虑数据讹误就能自由地共享和高速缓存对象引用.尽管不变性可能未必对于所有类都有意义,但大多数程序中至少有一些类将受益于不可变.在本月的 Java 理论与实践中,Brian Goetz 说明了不变性的一些长处和构造不变类的一些准则.请在附带的论坛中与作者和其他读者分享您关于本文的心得.(也可以单击文章顶部或底部的"讨论"来访问论坛.) 不变对象是指在实例化后其外部可见状态无法更改的对象.Java 类库中的 String.

Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制

伸缩 内容: synchronized 快速回顾 对 synchronized 的改进 比较 ReentrantLock 和 synchronized 的可伸缩性 条件变量 这不公平 结束语 参考资料 关于作者 对本文的评价 相关内容: Java 理论与实践 系列 Synchronization is not the enemy Reducing contention IBM developer kits for the Java platform (downloads) 订阅: develop

Java理论与实践: 您的小数点到哪里去了?

许多程序员在其整个开发生涯中都不曾使用定点或浮点数,可能的例外是, 偶尔在计时测试或基准测试程序中会用到.Java语言和类库支持两类非整数类型 ― IEEE 754 浮点( float 和 double ,包装类(wrapper class)为 Float 和 Double ),以及任意精度的小数( java.math.BigDecimal ).在本月的 Java 理论和实践中,Brian Goetz 探讨了在 Java 程序中使用非整数类型时一 些常碰到的陷阱和"gotcha". 虽

Java理论与实践: 垃圾收集简史

Java 语言可能是使用最广泛的依赖于垃圾收集的编程语言,但是它并不是第 一个.垃圾收集已经成为了包括 Lisp.Smalltalk.Eiffel.Haskell.ML. Scheme和 Modula-3 在内的许多编程语言的一个集成部分,并且从 20 世纪 60 年代早期就开始使用了.在 Java 理论与实践的本篇文章中,Brian Goetz 描述 了垃圾收集最常用的技术. 垃圾收集的好处是无可争辩的 ―― 可靠性提高.使内存管理与类接口设计 分离,并使开发者减少了跟踪内存管理错误的时间.著

Java理论与实践: 消除bug

很多有关编程风格的建议都是为了创建高质量.可维护的代码,这很合理, 因为最容易修复 bug 的时间就是在产生 bug 之前(少量的预防措施--).遗 憾的是,只预防往往是不够的,虽然有一些精巧的工具可以帮助您创建好的代码 ,但是很少有工具可以帮助您分析.维护或提高现有代码的质量. 写线程安全的类很难,而分析现有类的线程安全性更难,增强类使其仍然保 持线程安全也很难.以隐含假定.不变式以及预期用例(虽然在开发人员的头脑 中很清晰,但是没有以设计笔记.注释或者文档的方式记录下来)的方式编写完 类之后