`
jiasky
  • 浏览: 23344 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

多核系统上的 Java 并发缺

阅读更多

转摘至:http://www.ibm.com/developerworks/cn/java/j-concurrencybugpatterns/index.html

 

 

对于多线程编程经验较少的程序员而言,开发多核系统软件将面临两个方面的问题:首先,并发会给 Java 程序引入新的缺陷,如数据速度和死锁,它们是非常难以复现和发现的。其次,许多程序员并不知道特定多线程编程方法的微妙细节,而这可能会导致代码错误。

为了避免给并发程序引入缺陷,Java 程序员必须了解如何识别缺陷在多线程代码中很可能出现的关键位置,然后才能够编写出没有缺陷的软件。在本文中,我们将帮助 Java 开发人员在理解并发编程早期和中期会遇到的问题。我们并不会关注于常见的 Java 并发缺陷模式,如双重检查锁、循环等待和等待不在循环内项目,我们将介绍 6 个鲜为人知的模式,但是却经常出现在真实的 Java 应用程序中。事实上,我们的前两个例子就是在两个流行的 Web 服务器上发现的缺陷。

 

1. Jetty 的一个反模式

我们要介绍的第一个并发缺陷是在广泛使用的开源 HTTP 服务器 Jetty 上发现的。这是已经过 Jetty 社区确认的一个真实缺陷(见 参考资料 的缺陷报告)。


清单 1. 在一个易变(volatile)域上不获取锁的情况下执行非原子操作

// Jetty 7.1.0,
// org.eclipse.jetty.io.nio,
// SelectorManager.java, line 105

private volatile int _set;
......
public void register(SocketChannel channel, Object att)
{
   int s=_set++;
   ......
}
......
public void addChange(Object point)
{
   synchronized (_changes)
   {
      ......
   }
}
 

清单 1 中的错误有以下几个部分:

  • 首先,_set 被声明为 volatile ,这表示这个域可以由多个线程访问。
  • 但是, _set++ 并不是原子操作,这意味着它不会以单个不可分割操作执行。相反,它只是包含三个具体操作序列的简写方法:read-modify-write
  • 最后, _set++ 并没有锁保护。如果方法 register 同时由多个线程调用,那么它会产生一个竞争状态,导致出现错误的 _set 值。

您的代码也可能和 Jetty 一样出现这种类型的错误,所以让我更详细地分析一下它是如何发生的。

缺陷模式构成元素

分析它的逻辑执行代码有助于弄清楚这个缺陷模式。变量 i 的操作,如

i++
--i
i += 1
i -= 1
i *= 2
 

等,另外就是非原子操作(即 read-modify-write )。如果您知道 volatile 关键字在 Java 语言中仅仅保证变量的可见性,而不保证原子性,那么这应该会引起您的注意。一个易变域上的不受锁保护的非原子操作可能 会产生一个竞争状况 — 但是只有在多个线程并发访问非原子操作时才可能出现。

在一个线程安全的程序中,只有一个写线程能够修改这个变量;而其他的线程则可以读取 volatile 声明变量的最新值。

所以,代码是否有问题取决于有多少线程能够并发地访问这个操作。如果这个非原子操作仅仅由一个线程调用,由于是有一个开始联合关系或者外部锁,那么这样的编码方法也是线程安全的。

一定要谨记 volatile 关键字在 Java 代码中仅仅保证这个变量是可见的:它不保证原子性。在那些非原子且可由多个线程访问的易变操作中,一定不能够依赖于 volatile 的同步机制。相反,要使用 java.util.concurrent 包的同步语句、锁类和原子类。它们在设计上能够保证程序是线程安全的。

2. 在易变域上的同步

在 Java 语言中,我们使用了同步语句来获取互斥锁,这可以保护多线程系统的共享资源访问。然而,易变域的同步中会有一个漏洞,它可能破坏互斥。解决的方法是一定要将同步的域声明为 private final 。让我们先来仔细看看问题是如何产生的。

修改域上的同时访问锁

同步语句是由同步域所引用对象保护的,而不是由域本身保护的。如果一个同步域是易变的(这意味着这个域在初始化之后可能在程序的其他位置赋值),这很可能不是有用的语义,因为不同的线程可能同时访问不同的对象。

您可以在清单 2 中看到这个问题,这是节选自开源 Web 应用服务器 Tomcat 的代码片断:


清单 2. Tomcat 的错误

96: public void addInstanceListener(InstanceListener listener) {
97:
98:    synchronized (listeners) {
99:       InstanceListener results[] =
100:        new InstanceListener[listeners.length + 1];
101:      for (int i = 0; i < listeners.length; i++)
102:          results[i] = listeners[i];
103:      results[listeners.length] = listener;
104:      listeners = results;
105:   }
106:
107:}
 

假设 listeners 引用的是数组 A,而线程 T1 首先获取数组 A 的锁,然后开始创建数组 B。同时,T2 开始执行,并且由于数据 A 的锁而被阻挡。当 T1 完成数组 B 的 listeners 设置后,退出这个语句,T2 会锁住数组 A,然后开始复制数组 B。然后 T3 开始执行,并锁住数组 B。因为它们获得了不同的锁,T2 和 T3 现在可以同时复制数组 B。

图 1 更进一步地说明了这个执行顺序:


图 1. 由于易变域的同步而失去互斥锁

无数的意外行为可能会导致这种情况出现。至少,其中一个新的监听器可能会丢失,或者其中一个线程可能会发生 ArrayIndexOutOfBoundsException 异常(由于 listeners 引用及其长度可能在方法的任意时刻发生变化)。

好的做法是总是将同步域声明为 private final ,这能够保证锁对象保持不变,并且保证了互斥(mutex )。

3. java.util.concurrent 锁泄漏

一个实现 java.util.concurrent.locks.Lock 接口的锁控制着多个线程是如何访问一个共享资源的。这些锁不需要使用语句结构,所以它们比同步方法或语句更灵活。然而,这种灵活性可能导致编码错误,因为不使用语句的锁是不会自动释放的。如果一个 Lock.lock() 调用没有在同一个实例上执行相应的 unlock() 调用,其结果就可能造成一个锁泄漏。

如果忽视关键代码中的方法行为,我们就很容易造成 java.util.concurrent 锁泄漏,有可能抛出的异常。您可以从清单 3 的代码看到这一点,其中 accessResource 方法在访问共享资源时抛出了一个 InterruptedException 异常。结果,unlock() 是不会被调用的。


清单 3. 分析一个锁泄漏

private final Lock lock = new ReentrantLock();

public void lockLeak() {
   lock.lock();
   try {
      // access the shared resource
      accessResource();
      lock.unlock();
   } catch (Exception e) {}

public void accessResource() throws InterruptedException {...}
 

要保证锁得到释放,我们只需要在每一个 lock 之后对应执行一个 unlock 方法,而且它们应该置于 try-finally 复杂语句中。清单 4 说明了这种方法:


清单 4. 总是将 unlock 调用置于 finally 语句中

private final Lock lock = new ReentrantLock();

public void lockLeak() {
   lock.lock();
   try {
      // access the shared resource
      accessResource();
   } catch (Exception e) {}
   finally {
      lock.unlock();
   }

public void accessResource() throws InterruptedException {...}
 

4. 同步语句的性能优化

有一些并发缺陷有时不会使代码出错,但是它们可能会降低应用程序的性能。考虑清单 5 中的 synchronized 语句:


清单 5. 带有不变代码的同步语句

public class Operator {
   private int generation = 0; //shared variable
   private float totalAmount = 0; //shared variable
   private final Object lock = new Object();

   public void workOn(List<Operand> operands) {
      synchronized (lock) {
         int curGeneration = generation; //requires synch
         float amountForThisWork = 0;
         for (Operand o : operands) {
            o.setGeneration(curGeneration);
            amountForThisWork += o.amount;
         }
         totalAmount += amountForThisWork; //requires synch
         generation++; //requires synch
      }
   }
}
 

清单 5 代码中两个共享变量的访问是同步且正确的,但是如果仔细检查,您会注意到 synchronized 语句所需要进行的计算过多。我们可以通过调整代码顺序来解决这个问题,如清单 6 所示:


清单 6. 没有不变代码的同步语句

public void workOn(List<Operand> operands) {
   int curGeneration;
   float amountForThisWork = 0;
   synchronized (lock) {
      int curGeneration = generation++;
   }
   for (Operand o : operands) {
      o.setGeneration(curGeneration);
      amountForThisWork += o.amount;
   }
   synchronized (lock)
      totalAmount += amountForThisWork;
   }
}

 第二个版本代码在多核机器上执行效果会更好。其原因是清单 5 的同步代码阻止了并行执行。这个方法循环可能会消耗大量的计算时间。在清单 6 中,循环被移出同步语句,所以它可能由多个线程并行执行。一般而言,在保证线程安全的前提下要尽可能地简化同步语句。

5. 多阶段访问

假设您的应用程序有两个表:第一个表将员工姓名映射到一个员工号,另一个将这个员工号映射到一个薪水记录。这些数据需要支持并发访问和更新,而您可以通过线程安全的 ConcurrentHashMap 实现,如清单 7 所示:


清单 7. 两个阶段的访问

public class Employees {
   private final ConcurrentHashMap<String,Integer> nameToNumber;
   private final ConcurrentHashMap<Integer,Salary> numberToSalary;

   ... various methods for adding, removing, getting, etc...

   public int geBonusFor(String name) {
      Integer serialNum = nameToNumber.get(name);
      Salary salary = numberToSalary.get(serialNum);
      return salary.getBonus();
   }
}
 

这种方法看起来是线程安全的,但是事实上不是这样的。它的问题是 getBonusFor 方法并不是线程安全的。在获取这个序列号和使用它获取薪水之间,另一个线程可能从两个表删除员工信息。在这种情况下,第二个映射访问可能会返回 null ,并抛出一个异常。

保证每一个 Map 本身的线程安全是不够的。它们之间存在一个依赖关系,而且访问这两个 Map 的一些操作必须是原子操作。在这里,您可以使用非线程安全的容器(如 java.util.HashMap ),然后使用显式的同步语句来保护每一个访问,从而实现线程安全。然后这个同步语句可以在需要时包含这两个访问。

6. 对称锁死锁

可以考虑使用一个线程安全容器类 — 一个保证用户操作线程安全的数据结构。(这与 java.util 中的大多数容器不同,它不需要用户同步容器的使用。)在清单 8 中,一个可修改的成员变量负责保存数据,而一个锁对象则保护所有对它的访问。


清单 8. 一个线程安全的容器

public <E> class ConcurrentHeap {
   private E[] elements;
   private final Object lock = new Object(); //protects elements

   public void add (E newElement) {
      synchronized(lock) {
         ... //manipulate elements
      }
   }

   public E removeTop() {
      synchronized(lock) {
         E top = elements[0];
         ... //manipulate elements
         return top;
      }
   }
}
 

现在让我添加一个方法,使用另一个实例,并将它的所有元素添加到当前的实例中。这个方法需要访问这两个实例的 elements 成员,如清单 9 所示:


清单 9. 下面代码会产生一个死锁

public void addAll(ConcurrentHeap other) {
   synchronized(other.lock) {
      synchronized(this.lock) {
         ... //manipulate other.elements and this.elements
      }
   }
}

 您认识到了死锁的可能性吗?假设一个程序只有两个实例 heap1heap2 。如果其中一个线程调用了 heap1.addAll(heap2) ,而另一个线程同时调用 heap2.addAll(heap1) ,那么这两个线程就可能遇到死锁。换言之,假设第一个线程获得了 heap2 的锁,但是它开始执行之前,第二个线程就开始执行方法,同时获取了 heap1 锁。结果,每一个线程都会等待另一个线程所保持的锁。

分享到:
评论

相关推荐

    多核系统上的 Java 并发缺陷模式.doc

    在多核系统上进行Java并发编程时,程序员经常会遇到各种并发缺陷模式,这些模式可能导致程序的线程安全性和性能受到严重影响。以下六个鲜为人知的Java并发缺陷模式是本文的重点,旨在帮助开发者避免这些问题。 1. *...

    java并发编程2

    Java并发编程是Java开发中的重要领域,特别是在多核处理器和分布式系统中,高效地利用并发可以极大地提升程序的性能和响应速度。以下是对标题和描述中所提及的几个知识点的详细解释: 1. **线程与并发** - **线程*...

    java并发编程艺术

    《Java并发编程艺术》这本书深入探讨了Java平台上的并发编程技术。并发编程是现代多核处理器环境下提升软件性能的关键手段,而Java语言提供了丰富的工具和API来支持这一领域。本书旨在帮助开发者理解和掌握如何在...

    (PDF带目录)《Java 并发编程实战》,java并发实战,并发

    《Java 并发编程实战》是一本专注于Java并发编程的权威指南,对于任何希望深入了解Java多线程和并发控制机制的开发者来说,都是不可或缺的参考资料。这本书深入浅出地介绍了如何在Java环境中有效地管理和控制并发...

    java并发编程实战(英文版)

    综上所述,《Java并发编程实战》不仅涵盖了Java并发编程的基础知识和技术细节,还包含了丰富的实践经验和前瞻性的思考,是任何一位从事Java开发工作的程序员不可或缺的学习资源。无论是初学者还是有经验的开发者都能...

    java并发编程实践高清中文版+源码

    《JAVA并发编程实践》随着多核处理器的普及,使用并发成为构建高性能应用程序的关键。Java 5以及6在开发并发程序中取得了显著的进步,提高了Java虚拟机的性能以及并发类的可伸缩性,并加入了丰富的新并发构建块。在...

    java 并发编程的艺术pdf清晰完整版 源码

    并发编程是现代计算机系统中不可或缺的一部分,尤其是在多核处理器成为主流的今天。Java语言提供了丰富的并发工具和API,如线程、守护线程、线程池、同步机制(synchronized、wait/notify)、并发集合...

    java并发编程实战中文加英文版加源码

    直接就放在书上,你丫有没有良知,书籍是什么,是希望,是神圣的,你们这些译者简直就是在犯罪 ,不过要是英文功底不好,还是建议买本看吧,谁让你英文水平不如他们呢 《JAVA并发编程实践》随着多核处理器的普及,...

    java并发编程内部分享PPT

    Java并发编程是Java开发中的重要领域,特别是在多核处理器和分布式系统中,高效地利用并发可以极大地提升程序的性能和响应速度。这份“java并发编程内部分享PPT”显然是一个深入探讨这一主题的资料,旨在帮助开发者...

    Java并发编程实战华章专业开发者书库 (Tim Peierls 等 美Brian Goetz).pdf

    《Java并发编程实战》是一本深入探讨Java平台并发编程的权威指南,由Tim ...在多核处理器的时代,掌握并发编程技巧是每个Java开发者的必备能力,而《Java并发编程实战》无疑为此提供了一个全面而深入的学习资源。

    JAVA并发编程艺术pdf版

    《JAVA并发编程艺术》是Java开发者深入理解和掌握并发编程的一本重要著作,它涵盖了Java并发领域的核心概念和技术。这本书详细阐述了如何在多线程环境下有效地编写高效、可靠的代码,对于提升Java程序员的技能水平...

    Java并发编程实践高清pdf及源码

    《Java并发编程实践》是一本深入探讨Java多线程编程的经典著作,由Brian Goetz、Tim Peierls、Joshua Bloch、Joseph Bowles和David Holmes等专家共同编写。这本书全面介绍了Java平台上的并发编程技术,是Java开发...

    JAVA并发编程实践

    《JAVA并发编程实践》随着多核处理器的普及,使用并发成为构建高性能应用程序的关键。Java 5以及6在开发并发程序中取得了显著的进步,提高了Java虚拟机的性能以及并发类的可伸缩性,并加入了丰富的新并发构建块。在...

    JAVA并发编程实践.pdf+高清版+目录 书籍源码

    Java并发API包括了线程、锁、同步、并发容器等丰富的工具,使得开发者可以构建能够充分利用多核处理器性能的应用程序。本书详细介绍了这些主题,并提供了实例代码和实践建议。 首先,书中详细讨论了Java线程的创建...

    Java并发编程书籍高清版

    Java并发编程是Java开发中的重要领域,特别是在多核处理器和分布式系统中,高效地利用并发能力对于提升程序性能至关重要。本资源包含三本权威的Java并发编程书籍:《Java并发编程实践》、《java并发编程的艺术》以及...

    Java并发实战

    综上所述,《Java并发实战》这本书是从理论和实践两个维度来讲解Java并发编程,强调了对并发场景的分析和解决问题的实践能力,帮助读者克服学习并发编程时的难点。通过这本书,读者可以逐步建立起对并发编程的全面...

    Java并发编程实践.pdf

    ### Java并发编程实践 #### 一、并发编程基础 ##### 1.1 并发与并行的区别 在Java并发编程中,首先需要理解“并发”(Concurrency)和“并行”(Parallelism)的区别。“并发”指的是多个任务在同一时间段内交替...

    java并发实战中文文档

    Java并发编程是现代Java应用程序设计的关键组成部分,尤其在多核处理器和分布式系统中,合理地利用并发能够显著提高程序的性能和响应速度。Java平台提供了丰富的并发工具和API,如线程、锁、同步机制、并发集合以及...

    java并发编程

    Java并发编程是Java开发者必须掌握的关键技能之一,它涉及到如何在多线程环境中高效、安全地执行程序。并发编程能够充分利用多核处理器的计算能力,提高应用程序的响应速度和整体性能。《Java编程并发实战》这本书是...

    Java并发编程设计原则和模式

    本资料“Java并发编程设计原则和模式”深入探讨了如何在Java环境中有效地进行并发处理,以充分利用系统资源并避免潜在的并发问题。 一、并发编程基础 并发是指两个或多个操作在同一时间段内执行,但并不意味着这些...

Global site tag (gtag.js) - Google Analytics