A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 黄炳期 金牌黑马   /  2013-10-11 18:25  /  1123 人查看  /  0 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

 常见的上锁问题

  不幸的是,使用上锁会带来其他问题。让我们来看一些常见问题以及相应的解决方法:

  死锁。死锁是一个经典的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。假如线程 “A” 获得了刀,而线程 “B” 获得了叉。线程 A 就会进入阻塞状态来等待获得叉,而线程 B 则阻塞来等待 A 所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。虽然要探测或推敲各种情况是非常困难的,但只要按照下面几条规则去设计系统,就能够避免死锁问题:

  让所有的线程按照同样的顺序获得一组锁。这种方法消除了 X 和 Y 的拥有者分别等待对方的资源的问题。

  将多个锁组成一组并放到同一个锁下。前面死锁的例子中,可以创建一个银器对象的锁。于是在获得刀或叉之前都必须获得这个银器的锁。

  将那些不会阻塞的可获得资源用变量标志出来。当某个线程获得银器对象的锁时,就可以通过检查变量来判断是否整个银器集合中的对象锁都可获得。如果是,它就可以获得相关的锁,否则,就要释放掉银器这个锁并稍后再尝试。

  最重要的是,在编写代码前认真仔细地设计整个系统。多线程是困难的,在开始编程之前详细设计系统能够帮助你避免难以发现死锁的问题。

  Volatile 变量。 volatile 关键字是 Java 语言为优化编译器设计的。以下面的代码为例:

  class VolatileTest {

  public void foo() {

  boolean flag = false;

  if(flag) {

  // this could happen

  }

  }

  }

  一个优化的编译器可能会判断出 if 部分的语句永远不会被执行,就根本不会编译这部分的代码。如果这个类被多线程访问,flag 被前面某个线程设置之后,在它被 if 语句测试之前,可以被其他线程重新设置。用 volatile 关键字来声明变量,就可以告诉编译器在编译的时候,不需要通过预测变量值来优化这部分的代码。

  无法访问的线程 有时候虽然获取对象锁没有问题,线程依然有可能进入阻塞状态。在 Java 编程中 IO 就是这类问题最好的例子。当线程因为对象内的 IO 调用而阻塞时,此对象应当仍能被其他线程访问。该对象通常有责任取消这个阻塞的 IO 操作。造成阻塞调用的线程常常会令同步任务失败。如果该对象的其他方法也是同步的,当线程被阻塞时,此对象也就相当于被冷冻住了。其他的线程由于不能获得对象的锁,就不能给此对象发消息(例如,取消 IO 操作)。必须确保不在同步代码中包含那些阻塞调用,或确认在一个用同步阻塞代码的对象中存在非同步方法。尽管这种方法需要花费一些注意力来保证结果代码安全运行,但它允许在拥有对象的线程发生阻塞后,该对象仍能够响应其他线程。

  为不同的线程模型进行设计

  判断是抢占式还是协作式的线程模型,取决于虚拟机的实现者,并根据各种实现而不同。因此,Java 开发员必须编写那些能够在两种模型上工作的程序。

  正如前面所提到的,在抢占式模型中线程可以在代码的任何一个部分的中间被打断,除非那是一个原子操作代码块。原子操作代码块中的代码段一旦开始执行,就要在该线程被换出处理器之前执行完毕。在 Java 编程中,分配一个小于 32 位的变量空间是一种原子操作,而此外象 double 和 long 这两个 64 位数据类型的分配就不是原子的。使用锁来正确同步共享资源的访问,就足以保证一个多线程程序在抢占式模型下正确工作。

  而在协作式模型中,是否能保证线程正常放弃处理器,不掠夺其他线程的执行时间,则完全取决于程序员。调用 yield() 方法能够将当前的线程从处理器中移出到准备就绪队列中。另一个方法则是调用 sleep() 方法,使线程放弃处理器,并且在 sleep 方法中指定的时间间隔内睡眠。

  正如你所想的那样,将这些方法随意放在代码的某个地方,并不能够保证正常工作。如果线程正拥有一个锁(因为它在一个同步方法或代码块中),则当它调用 yield() 时不能够释放这个锁。这就意味着即使这个线程已经被挂起,等待这个锁释放的其他线程依然不能继续运行。为了缓解这个问题,最好不在同步方法中调用 yield 方法。将那些需要同步的代码包在一个同步块中,里面不含有非同步的方法,并且在这些同步代码块之外才调用 yield.

  另外一个解决方法则是调用 wait() 方法,使处理器放弃它当前拥有的对象的锁。如果对象在方法级别上使同步的,这种方法能够很好的工作。因为它仅仅使用了一个锁。如果它使用 fine-grained 锁,则 wait() 将无法放弃这些锁。此外,一个因为调用 wait() 方法而阻塞的线程,只有当其他线程调用 notifyAll() 时才会被唤醒。

  线程和AWT/Swing

  在那些使用 Swing 和/或 AWT 包创建 GUI (用户图形界面)的 Java 程序中,AWT 事件句柄在它自己的线程中运行。开发员必须注意避免将这些 GUI 线程与较耗时间的计算工作绑在一起,因为这些线程必须负责处理用户时间并重绘用户图形界面。换句话来说,一旦 GUI 线程处于繁忙,整个程序看起来就象无响应状态。Swing 线程通过调用合适方法,通知那些 Swing callback (例如 Mouse Listener 和 Action Listener )。 这种方法意味着 listener 无论要做多少事情,都应当利用 listener callback 方法产生其他线程来完成此项工作。目的便在于让 listener callback 更快速返回,从而允许 Swing 线程响应其他事件。

  如果一个 Swing 线程不能够同步运行、响应事件并重绘输出,那怎么能够让其他的线程安全地修改 Swing 的状态?正如上面提到的,Swing callback 在 Swing 线程中运行。因此他们能修改 Swing 数据并绘到屏幕上。

  但是如果不是 Swing callback 产生的变化该怎么办呢?使用一个非 Swing 线程来修改 Swing 数据是不安全的。Swing 提供了两个方法来解决这个问题:invokeLater() 和 invokeAndWait()。为了修改 Swing 状态,只要简单地调用其中一个方法,让 Runnable 的对象来做这些工作。因为 Runnable 对象通常就是它们自身的线程,你可能会认为这些对象会作为线程来执行。但那样做其实也是不安全的。事实上,Swing 会将这些对象放到队列中,并在将来某个时刻执行它的 run 方法。这样才能够安全修改 Swing 状态。

  总结

  Java 语言的设计,使得多线程对几乎所有的 Applet 都是必要的。特别是,IO 和 GUI 编程都需要多线程来为用户提供完美的体验。如果依照本文所提到的若干基本规则,并在开始编程前仔细设计系统--包括它对共享资源的访问等,你就可以避免许多常见和难以发觉的线程陷阱。

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马