JAVA 线程架构
Java 多线程编程其实并不象大多数的书描述的那样简单,所有关于UI(用户界面)的Java编程都要涉及多线程。这一章将会通过讨论几种操作系统的线程架构和这些架构将会怎样影响Java多线程编程。按照这样思路,我将介绍一些在Java的入门级书籍中描述的不慎清楚的关键术语和概念。理解这些概念是使你能够看懂本书所提供的例子的必备条件。
多线程编程的问题
象鸵鸟一样的把自己的头埋在沙子里,假装不去考虑多线程的问题其实是目前很多人进行Java编程共同弊病。但是在真正的产品中,你却无法回避这个严重的问题。目前,市面上大多数的书对Java线程的描述都是很肤浅的,甚至它们提供的例子本身就无法在多线程的环境下正确运行。
事实上,多线程是影响所有代码的重要因素。可以极端一点的说,单线程的代码在现实应用中,一钱不值,甚至根本无法运行,更不用说正确性和高效率了。所以你应该从一开始就把多线程作为一个重要的方面,融入你的代码架构。
所有不平凡的Java程序都是多线程的
不管你喜欢与否,所有的Java程序除了小部分非常简单的控制台程序都是基于多线程的。原因在于Java的Abstract Windowing Toolkit ( AWT )和它的扩展Swing,AWT用一个特殊的线程处理所有的操作系统级的事件,这个特殊的线程是在第一个窗口出现的时候产生的。因此,几乎所有的AWT程序都有至少2个线程在运行:一个是main函数所在的线程和处理来自OS的事件和调用注册的监听者的响应方法(也就是回调函数)的AWT线程。必须注意的是所有注册的监听者方法,运行在AWT线程上,而不是人们一般认为的main函数(这也是监听器注册的线程)。
这种架构有两个问题。第一,虽然监听器的方法是运行在AWT线程上的,但是他们其实都是在main线程上声明的内部类(inner-class)。第二,虽然监听器的方法是运行在AWT线程上的,但是它一般会非常频繁的访问它的外部类,也就是运行在main线程上的类的成员变量。当这两个线程竞争(compete)访问同一个对象实例(Object)时,会引起非常严重的线程同步问题。适当的使用关键字synchronized是保证两个线程安全访问共享对象的必要手段。
更糟的是,AWT线程不但止处理监听器方法,还有响应来自操作系统的事件。这就意味着,如果你的监听器方法占用大量的CPU时间来进行处理,则你的程序将无法响应操作系统级的事件(例如鼠标点击事件和键盘事件)。这些事件将会被阻塞在事件队列中,直到监听器方法返回。具体的表现就是UI的死锁。这样会让用户无法接受的。Listing 1.1就是这样一个无响应UI的例子。程序产生一个包含两个按钮的Frame。Sleep按钮使它所在的线程(也就是前面所说的AWT事件处理线程)休眠5秒钟。Hello按钮只是简单的在控制台上打印“Hello World”。在你按下Sleep按钮5秒钟之内,无论你按多少次Hello按钮,程序都不会有任何响应。如果你在这期间按下了Hello按钮5次。那么“Hello World”将会立即被连续打印五次,当你Sleep按钮的监听器方法结束以后。这就证明了5个鼠标点击事件被阻塞在事件队列里,直到Sleep按钮的事件响应完。
import javax.swing.*; import java.awt.*; import java.awt.event.*;
class Hang extends JFrame { public Hang() { JButton b1 = new JButton( "Sleep" ); JButton b2 = new JButton( "Hello" );
b1.addActionListener ( new ActionListener() { public void actionPerformed( ActionEvent event ) { try { Thread.currentThread().sleep(5000); } catch(Exception e){} } } );
b2.addActionListener ( new ActionListener() { public void actionPerformed( ActionEvent event ) { System.out.println("Hello world"); } } );
getContentPane().setLayout( new FlowLayout() ); getContentPane().add( b1 ); getContentPane().add( b2 ); pack(); show(); }
public static void main( String[] args ) { new Hang(); } } |
大多数的书籍中讨论的Java GUI都回避了线程的问题。在现实中,对于UI事件采取单线程的方法都是不可取的。所有成功的UI程序都有下面几个共同点:
l UI必须就程序的运行状态进程,给用户一些回馈信息。简单的弹出一个显示程序正在做的事情的对话框是不足够的。你必须告诉用户操作的运行进度(例如一个带有百分比的进度条)。
l 必须做到当底层系统状态改变时,不会为了更新窗口而把整个UI重绘。
l 你必须使你的程序做到,当用户点击Cancel按钮时,你的程序能够立即响应,并及时终止。
l 必须做到当一个需要长时间运行的操作正在运行时,用户可以在你的UI界面上做其它的操作。
这是条规则可以用一句话来总结:不允许死锁的UI界面出现,不允许当程序运行一个
耗时很长的操作时,忽略掉用户其他的操作,如鼠标点击和键盘事件,因此不允许在监听器窗口中,运行长时间的操作。耗时操作必须在后台的其他线程运行。因此,真正的程序在任何时候都会2个以上的线程在跑。