小洛 的个人资料Paul's照片日志列表 工具 帮助

日志


2006/3/22

澄清 Java 的接口与继承机制

    大多数人认为,接口的意义在于顶替多重继承。众所周知Java没有c++那样多重继承的机制,但是却能够实现多个接口。其实这样做是很牵强的,接口和继承是完全不同的东西,接口没有能力代替多重继承,也没有这个义务。接口的作用,一言以蔽之,就是标志类的类别(type of class)。把不同类型的类归于不同的接口,可以更好的管理他们。Java的精髓,我认为,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如c++、java、c#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。

  设计模式中最基础的是工厂模式(Factory),在我最近的一个很简单的应用中,我想尽量的让我的程序能够在多个数据库间移植,当然,这涉及很多问题,单是如何兼容不同DBMS的SQL就让人头痛。我们不妨先把问题简单化,只考虑如何连接不同的数据库。

  假设我有很多个类,分别是Mysql.java、SQLServer.java、Oracle.java、DB2.java,他们分别连接不同的数据库,统一返回一个Connection对象,并且都有一个close方法,用于关闭连接。只需要针对你的DBMS,选择不同的类,就可以用了,但是我的用户他会使用什么数据库?我不知道,我希望的是尽量少的修改代码,就能满足他的需要。我可以抽象如下接口:


  package org.bromon.test;
  public interface DB
  {
  java.sql.Connection openDB(String url,String user,String password);
  void close();
  }

 

  这个接口只定义两个方法,没有任何有实际意义的代码,具体的代码由实作这个接口的类来给出,比如Mysql.java:


  Package org.bromon.test;
  import java.sql.*;
  public class Mysql implements DB
  {
  private String url=”jdbc:mysql:localhost:3306/test”;
  private String user=”root”;
  private String password=””;
  private Connection conn;
  public Connection openDB(url,user,password)
  {
    //连接数据库的代码
  }

  public void close()
  {
    //关闭数据库
  }
  }

  类似的当然还有Oracle.java等等,接口DB给这些类归了个类,在应用程序中我们这样定义对象:

  org.bromon.test.DB myDB;

 

  使用myDB来操作数据库,就可以不用管实际上我所使用的是哪个类,这就是所谓的“开-闭”原则。但是问题在于接口是不能实例化的,myDB=new DB(),这样的代码是绝对错误的,我们只能myDB=new Mysql()或者myDB=new Oracle()。麻烦了,我还是需要指定具体实例化的是哪个类,用了接口跟没用一样。所以我们需要一个工厂:


  package org.bromon.test;
  public class DBFactory
  {
  public static DB Connection getConn()
  {
    Return(new Mysql());
  }
  }

 

  所以实例化的代码变成:myDB=DBFactory.getConn();这就是23种模式中最基础的普通工厂(Factory),工厂类负责具体实例化哪个类,而其他的程序逻辑都是针对DB这个接口进行操作,这就是“针对接口编程”。责任都被推卸给工厂类了,当然你也可以继续定义工厂接口,继续把责任上抛,这就演变成抽象工厂(Abstract Factory)。

  整个过程中接口不负责任何具体操作,其他的程序要连接数据库的话,只需要构造一个DB对象,而不管工厂类如何变化。这就是接口的意义----抽象。继承的概念不用多说,很好理解。为什么要继承呢?因为你想重用代码?这绝对不是理由,继承的意义也在于抽象,而不是代码重用。如果对象A有一个run()方法,对象B也想有这个方法,所以有人就Class B extends A。这是不经大脑的做法。如果在B中实例化一个A,调用A的Run()方法,是不是可以达到同样的目的?如下:


  Class B
  {
  A a=new A();
  a.run();
  }


  这就是利用类的聚合来重用代码,是委派模式的雏形。那么继承的意义何在?其实这是历史原因造成的,请一定注意,继承的本意在于抽象,而非代码重用(虽然继承也有这个作用),这是初涉Java时最容易犯也是最严重的错误之一,在编程的过程中这个错误所造成的阴影,也许会对你的编程生涯造成严重的影响。什么时候应该使用继承?只在抽象类中使用,其他情况下尽量不使用。抽象类也是不能实例化的,它仅仅提供一个模版而已,这就很能说明问题。

2005/5/11

在Java中运用Hashtables

Hashtables提供了一个很有用的方法可以使应用程序的性能达到最佳。
by Pete Ford
Hashtables(哈希表)在计算机领域中已不是一个新概念了。它们是用来加快计算机的处理速度的,用当今的标准来处理,速度非常慢,而它们可以让你在查询许多数据条目时,很快地找到一个特殊的条目。尽管现代的机器速度已快了几千倍,但是为了得到应用程序的最佳性能,hashtables仍然是个很有用的方法。

设想一下,你有一个包含约一千条记录的数据文件——比如一个小企业的客户记录——还有一个程序,它把记录读到内存中进行处理。每个记录包含一个唯一的五位数的客户ID号、客户名字、地址、帐户结余等等。假设记录不是按客户ID号顺序分类的,所以,如果程序要将客户号作为“key” 来查找一个特殊的客户记录,唯一的查找方法就是连续地搜索每个记录。有时侯,它会很快找到你需要的记录;但有时侯,在程序找到你需要的记录前,它几乎已搜索到了最后一条记录。如果要在1,000条记录中搜索,那么查找任何一条记录都需要程序平均查核500.5 ((1000 + 1 )/2)条记录。如果你常需要查找数据,你应该需要一个更快的方法来找到一条记录。

 
图1. 方法变得越来越好
一种加快搜索的方法就是把记录分成几段,这样,你就不用搜索一个很大的列表了,而是搜索几个短的列表。对于我们数字式的客户ID号,你可以建10个列表——以0开头的ID号组成一个列表,以1开头的ID号组成一个列表,依此类推。那么要查找客户ID号38016,你只需要搜索以3开头的列表就行了。如果有1,000条记录,每个列表的平均长度为100(1,000条记录被分成10个列表),那么搜索一条记录的平均比较次数就降到了约50(见图1)。

当然,如果约十分之一的客户号是以0开头的,另外十分之一是以1开头的,等等,那么这种方法会很适合。如果90%的客户号以0开头,那么那个列表就会有900条记录,每次查找平均需要进行450次比较。另外,程序需要执行的搜索有90%都是针对以0开头的号码的。因此,平均比较数就大大超过简单数学运算的范围了。

 


如果我们可以按这样一种方式在我们的列表中分配记录,情况就会好一些,即每个列表约有相同条目的记录,而不管键值中数字的分布。我们需要一种方法能够把客户号码混合到一起并更好地分布结果。例如,我们可以取号码中的每位数,乘以某个大的数(随着数字位置的不同而不同), 然后将结果相加产生一个总数,把这个数除以10,并将余数作为索引值(index)。当读入记录时,程序在客户号码上运行这个哈希(hash) 函数来确定记录属于哪个列表。当用户需要查询时,将同一个哈希函数作为一个“key”用于客户号码,这样就可以搜索正确的列表了。 像这样的一个数据结构就称为一个哈希表(hashtable)。

Java中的Hashtables
Java包含两个类,java.util.Hashtable 和java.util.HashMap,它们提供了一个多种用途的hashtable机制。这两个类很相似,通常提供相同的公有接口。但它们的确有一些重要的不同点,我在后面会讲到。

Hashtable和HashMap对象可以让你把一个key和一个value结合起来,并用put() 方法把这对key/value输入到表中。然后你可以通过调用get()方法,把key作为参数来得到这个value(值)。只要满足两个基本的要求,key和value可以是任何对象。注意,因为key和value必须是对象,所以原始类型(primitive types)必须通过运用诸如Integer(int)的方法转换成对象。

为了将一个特定类的对象用做一个key,这个类必须提供两个方法,equals() 和 hashCode()。这两个方法在java.lang.Object中,所以所有的类都可以继承这两个方法;但是,这两个方法在Object类中的实现一般没什么用,所以你通常需要自己重载这两个方法。

Equals()方法把它的对象同另一个对象进行比较,如果这两个对象代表相同的信息,则返回true。该方法也查看并确保这两个对象属于相同的类。如果两个参照对象是完全一样的对象,Object.equals()返回true,这就说明了为什么这个方法通常不是很适合的原因。在大多数情况下,你需要一个方法来一个字段一个字段地进行比较,所以我们认为代表相同数据的不同对象是相等的。

HashCode()方法通过运用对象的内容执行一个哈希函数来生成一个int值。Hashtable和HashMap用这个值来算出一对key/value位于哪个bucket(哈希元)(或列表)中。

作为例子,我们可以查看一下String 类,因为它有自己的方法来实现这两个方法。String.equals()对两个String对象一个字符一个字符地进行比较,如果字符串是相同的,则返回true:

 


String myName = "Einstein";
// The following test is
// always true
if ( myName.equals("Einstein") )
{ ...

 


String.hashCode()在一个字符串上运行哈希函数。字符串中每个字符的数字代码都乘以31,结果取决于字符串中字符的位置。然后将这些计算的结果相加,得到一个总数。这个过程似乎很复杂,但是它确保能够更好地分布值。它也证明了你在开发你自己的hashCode()方法时,能够走多远,确信结果是唯一的。

例如,假设我要用一个hashtable来实现一个书的目录,把书的ISBN号码作为搜索键来进行搜索。我可以用String类来承载细节,并准备好了equals()和hashCode()方法(见列表1)。我们可以用put()方法添加成对的key/value到hashtable中(见列表2)。

Put()方法接受两个参数,它们都属于Object类型。第一个参数是key;第二个参数是value。Put()方法调用key的hashCode()方法,用表中的列表数来除这个结果。把余数作为索引值来确定该条记录添加到哪个列表中。注意,key在表中是唯一的;如果你用一个已经存在的key来调用put(),匹配的条目就被修改了,因此它参照的是一个新的值,而旧的值被返回了(当key在表中不存在时,put()返回空值)。

要读取表中的一个值,我们把搜索键用于get()方法。它返回一个转换到正确类型的Object参照:BookRecord br =
   (BookRecord)isbnTable.get(
   "0-345-40946-9");
System.out.println(
   "Author: " + br.author
   + " Title: " + br.title);
 


另一个有用的方法是remove(),其用法同get()几乎一样,它把条目从表中删除,并返回给调用程序。

你自己的类
如果你想把一个原始类型用做一个key,你必须创建一个同等类型的对象。例如,如果你想用一个整数key,你应该用构造器Integer(int)从整数中生成一个对象。所有的封装类——如Integer、Float和Boolean——都把原始值看做是对象,它们重载了equals()和hashCode()方法,因此,它们可以被用做key。JDK中提供的许多其它的类也是这样的(甚至Hashtable和HashMap类都实现它们自己的equals()和hashCode()方法),但你把任何类的对象用做hashtable keys前,应该查看文件。查看类的来源,看看equals()和hashCode()是如何实现的,也很有必要。例如,Byte、Character、Short和Integer都返回所代表的整数值作为哈希码。这可能适合,也可能不适合你的需求。

 


如果你想创建一个hashtable,这个hashtable运用你自己定义的一个类的对象作为key,那么你应该确信这个类的equals()和hashCode()方法提供有用的值。首先查看你扩展的类,确定它的实现是否满足你的需求。如果没有,你应该重载方法。

任何equals()方法的基本设计约束是,如果传递给它的对象属于同一个类,而且它的数据字段设定为表示同样数据的值,那么它就应该返回true。你也应该确信,如果传递一个空的参数给该方法,那么你的代码返回false:public boolean equals(Object o)
{
   if ( (o == null)
      || !(o instanceof myClass))
   {
      return false;
   }

   // Now compare data fields...

 


另外,在设计一个hashCode()方法时,应该记住一些规则。首先,该方法必须为一个特定的对象返回相同的值,而不管这个方法被调用了多少次(当然,只要对象的内容在调用之间没有改变,在将一个对象用做一个hashtable的key时,应该避免这一点)。第二,如果由你的equals()方法定义的两个对象是相等的,那么它们也必须生成相同的哈希码。第三,这更像是一个方针,而不是一个原则,你应该设法设计方法,使它为不同的对象内容生成不同的结果。如果偶尔不同的对象正好生成了相同的哈希码,这也不要紧。但是,如果该方法只能返回范围在1到10的值,那么只能用10个列表,而不管在hashtable中有多少个列表。

在设计equals()和hashCode()时,另一个要记住的因素是性能问题。每次调用put()或get(),都包括调用hashCode()来查找正确的列表,当get()扫描列表来查找key时,它为列表中的每个元素调用equals()。实现这些方法使它们尽可能快而有效地运行,尤其当你打算使你的类公开可用时,因为其它的用户可能想在执行速度很重要的情况下,在高性能的应用程序中运用你的类。

Hashtable性能
影响hashtable功效的主要因素就是表中列表的平均长度,因为平均搜索时间与这个平均长度直接相关。很显然,要减小平均长度,你必须增加hashtable中列表的数量;如果列表数量非常大,以至于大多数列表或所有列表只包含一条记录,你就会获得最佳的搜索效率。然而,这样做可能太过分了。如果你的hashtable的列表数远远多于数据条目,那你就没有必要做这样的内存花费了,而在一些情况下,人们也不可能接受这样的做法。

 


在我们前面的例子中,我们预先知道我们有多少条记录——1,000。知道这点后,我们就可以决定我们的hashtable应该包含多少个列表,以便达成搜索速度和内存使用效率之间最好的折衷方式。然而,在许多情况下,你预先不知道你要处理多少条记录;数据被读取的文件可能会不断扩大,或者记录的数量可能一天一天地发生很大的变化。

随着条目的增加,Hashtable和HashMap类通过动态地扩展表来处理这个问题。这两个类都有接受表中列表最初数量的构造器,和一个作为参数的负载系数(load factor):public Hashtable(
   int initialCapacity,
   float loadFactor)

public HashMap(
   int initialCapacity,
   float loadFactor)

 


将这两个数相乘计算出一个临界值。每次给哈希表添加一个新的条目时,计数就被更新,当计数超过临界值时,表被重新设置(rehash)。(列表数量增加到以前数量的两倍加1,所有的条目转移到正确的列表中。)缺省的构造器设定最初的容量为11,负载系数是0.75,所以临界值是8。当第九条记录被添加到表中时,就重新调整哈希表,使其有23个列表,新的临界值将是17(23*0.75的整数部分)。你可以看到,负载系数是哈希表中平均列表数量的上限,这就意味着,在缺省情况下,哈希表很少会有许多包含不只一条记录的列表。比较我们最初的例子,在那个例子中,我们有1,000条记录,分布在10个列表中。如果我们用缺省值,这个表将会扩展到含有1,500多个列表。但你可以控制这点。如果用负载系数相乘的列表数量大于你处理的条目数,那么表永远不会重制,所以我们可以仿效下面的例子:// Table will not rehash until it
// has 1,100 entries (10*110):
Hashtable myHashTable =
   new Hashtable(10, 110.0F);

 


你可能不想这么做,除非你没有为空的列表节省内存,而且不介意额外的搜索时间,这可能在嵌入系统中会出现这种情况。然而,这种方法可能很有用,因为重新设置很占用计算时间,而这种方法可以保证永远不会发生重新设置这种情况。

注意,虽然调用put()可以使表增大(列表数量增加),调用remove()不会有相反的结果。所以,如果你有一个大的表,而且从中删除了大部分条目,结果你会有一个大的但是大部分是空的表。

 


Hashtable和HashMap
Hashtable和HashMap类有三个重要的不同之处。第一个不同主要是历史原因。Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现。

也许最重要的不同是Hashtable的方法是同步的,而HashMap的方法不是。这就意味着,虽然你可以不用采取任何特殊的行为就可以在一个多线程的应用程序中用一个Hashtable,但你必须同样地为一个HashMap提供外同步。一个方便的方法就是利用Collections类的静态的synchronizedMap()方法,它创建一个线程安全的Map对象,并把它作为一个封装的对象来返回。这个对象的方法可以让你同步访问潜在的HashMap。这么做的结果就是当你不需要同步时,你不能切断Hashtable中的同步(比如在一个单线程的应用程序中),而且同步增加了很多处理费用。

第三点不同是,只有HashMap可以让你将空值作为一个表的条目的key或value。HashMap中只有一条记录可以是一个空的key,但任意数量的条目可以是空的value。这就是说,如果在表中没有发现搜索键,或者如果发现了搜索键,但它是一个空的值,那么get()将返回null。如果有必要,用containKey()方法来区别这两种情况。

一些资料建议,当需要同步时,用Hashtable,反之用HashMap。但是,因为在需要时,HashMap可以被同步,HashMap的功能比Hashtable的功能更多,而且它不是基于一个陈旧的类的,所以有人认为,在各种情况下,HashMap都优先于Hashtable。

关于Properties
有时侯,你可能想用一个hashtable来映射key的字符串到value的字符串。DOS、Windows和Unix中的环境字符串就有一些例子,如key的字符串PATH被映射到value的字符串C:\WINDOWS;C:\WINDOWS\SYSTEM。Hashtables是表示这些的一个简单的方法,但Java提供了另外一种方法。

Java.util.Properties类是Hashtable的一个子类,设计用于String keys和values。Properties对象的用法同Hashtable的用法相象,但是类增加了两个节省时间的方法,你应该知道。

Store()方法把一个Properties对象的内容以一种可读的形式保存到一个文件中。Load()方法正好相反,用来读取文件,并设定Properties对象来包含keys和values。

注意,因为Properties扩展了Hashtable,你可以用超类的put()方法来添加不是String对象的keys和values。这是不可取的。另外,如果你将store()用于一个不包含String对象的Properties对象,store()将失败。作为put()和get()的替代,你应该用setProperty()和getProperty(),它们用String参数。

好了,我希望你现在可以知道如何用hashtables来加速你的处理了。


关于作者:
Pete Ford已经做了20多年的软件开发工作了,他主要在嵌入系统和turnkey system方面进行研究。他生活并工作于德克萨斯州的达拉斯。你可以通过p_ford@mindspring.com联系他。

 
 
 

2005/3/29

JAVA专题技术综述之线程篇

 编写具有多线程能力的程序经常会用到的方法有:

  run(),start(),wait(),notify(),notifyAll(),sleep(),yield(),join()

  还有一个重要的关键字:synchronized

  本文将对以上内容进行讲解。

  一:run()和start()

  示例1:

public class ThreadTest extends Thread
{
public void run()
{
for(int i=0;i<10;i++)
{
System.out.print(" " + i);
}
}
public static void main(String[] args)
{
new ThreadTest().start();
new ThreadTest().start();
}

  这是个简单的多线程程序。run()和start()是大家都很熟悉的两个方法。把希望并行处理的代码都放在run()中;stat()用于自动调用run(),这是JAVA的内在机制规定的。并且run()的访问控制符必须是public,返回值必须是void(这种说法不准确,run()没有返回值),run()不带参数。

  这些规定想必大家都早已知道了,但你是否清楚为什么run方法必须声明成这样的形式?这涉及到JAVA的方法覆盖和重载的规定。这些内容很重要,请读者参考相关资料。

  二:关键字synchronized

  有了synchronized关键字,多线程程序的运行结果将变得可以控制。synchronized关键字用于保护共享数据。请大家注意"共享数据",你一定要分清哪些数据是共享数据,JAVA是面向对象的程序设计语言,所以初学者在编写多线程程序时,容易分不清哪些数据是共享数据。请看下面的例子:

  示例2:

public class ThreadTest implements Runnable
{

public synchronized void run()
{
for(int i=0;i<10;i++)
{
System.out.print(" " + i);
}
}
public static void main(String[] args)
{
Runnable r1 = new ThreadTest();
Runnable r2 = new ThreadTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}

  在这个程序中,run()被加上了synchronized关键字。在main方法中创建了两个线程。你可能会认为此程序的运行结果一定为:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。但你错了!这个程序中synchronized关键字保护的不是共享数据(其实在这个程序中synchronized关键字没有起到任何作用,此程序的运行结果是不可预先确定的)。这个程序中的t1,t2是两个对象(r1,r2)的线程。JAVA是面向对象的程序设计语言,不同的对象的数据是不同的,r1,r2有各自的run()方法,而synchronized使同一个对象的多个线程,在某个时刻只有其中的一个线程可以访问这个对象的synchronized数据。每个对象都有一个"锁标志",当这个对象的一个线程访问这个对象的某个synchronized数据时,这个对象的所有被synchronized修饰的数据将被上锁(因为"锁标志"被当前线程拿走了),只有当前线程访问完它要访问的synchronized数据时,当前线程才会释放"锁标志",这样同一个对象的其它线程才有机会访问synchronized数据。

  示例3:

public class ThreadTest implements Runnable
{
public synchronized void run()
{
for(int i=0;i<10;i++)
{
System.out.print(" " + i);
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();

t2.start();
}

  如果你运行1000次这个程序,它的输出结果也一定每次都是:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。因为这里的synchronized保护的是共享数据。t1,t2是同一个对象(r)的两个线程,当其中的一个线程(例如:t1)开始执行run()方法时,由于run()受synchronized保护,所以同一个对象的其他线程(t2)无法访问synchronized方法(run方法)。只有当t1执行完后t2才有机会执行。

  示例4:

public class ThreadTest implements Runnable
{
public void run()
{
synchronized(this)
{
for(int i=0;i<10;i++)
{
System.out.print(" " + i);
}
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}

  这个程序与示例3的运行结果一样。在可能的情况下,应该把保护范围缩到最小,可以用示例4的形式,this代表"这个对象"。没有必要把整个run()保护起来,run()中的代码只有一个for循环,所以只要保护for循环就可以了。

  示例5:

public class ThreadTest implements Runnable
{
public void run()
{
for(int k=0;k<5;k++)
{
System.out.println(Thread.currentThread().getName()
+ " : for loop : " + k);

}
synchronized(this)
{
for(int k=0;k<5;k++)
{
System.out.println(Thread.currentThread().getName()
+ " : synchronized for loop : " + k);
}
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t1 = new Thread(r,"t1_name");
Thread t2 = new Thread(r,"t2_name");
t1.start();
t2.start();
}
}
运行结果: t1_name : for loop : 0
t1_name : for loop : 1
t1_name : for loop : 2
t2_name : for loop : 0
t1_name : for loop : 3
t2_name : for loop : 1
t1_name : for loop : 4
t2_name : for loop : 2
t1_name : synchronized for loop : 0
t2_name : for loop : 3
t1_name : synchronized for loop : 1
t2_name : for loop : 4
t1_name : synchronized for loop : 2
t1_name : synchronized for loop : 3
t1_name : synchronized for loop : 4
t2_name : synchronized for loop : 0
t2_name : synchronized for loop : 1
t2_name : synchronized for loop : 2
t2_name : synchronized for loop : 3
t2_name : synchronized for loop : 4 

  第一个for循环没有受synchronized保护。对于第一个for循环,t1,t2可以同时访问。运行结果表明t1执行到了k=2时,t2开始执行了。t1首先执行完了第一个for循环,此时还没有执行完第一个for循环(t2刚执行到k=2)。t1开始执行第二个for循环,当t1的第二个for循环执行到k=1时,t2的第一个for循环执行完了。t2想开始执行第二个for循环,但由于t1首先执行了第二个for循环,这个对象的锁标志自然在t1手中(synchronized方法的执行权也就落到了t1手中),在t1没执行完第二个for循环的时候,它是不会释放锁标志的。所以t2必须等到t1执行完第二个for循环后,它才可以执行第二个for循环。

 三:sleep()

  示例6:

public class ThreadTest implements Runnable
{
public void run()
{
for(int k=0;k<5;k++)
{
if(k == 2)
{
try
{
Thread.currentThread().sleep(5000);
}
catch(Exception e)
{}
}
System.out.print(" " + k);
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t = new Thread(r);
t.start();
}

  sleep方法会使当前的线程暂停执行一定时间(给其它线程运行机会)。读者可以运行示例6,看看结果就明白了。sleep方法会抛出异常,必须提供捕获代码。

  示例7:

public class ThreadTest implements Runnable
{
public void run()
{

for(int k=0;k<5;k++)
{
if(k == 2)
{
try
{
Thread.currentThread().sleep(5000);
}
catch(Exception e)
{}
}
System.out.println(Thread.currentThread().getName()
+ " : " + k);
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t1 = new Thread(r,"t1_name");
Thread t2 = new Thread(r,"t2_name");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}

  t1被设置了最高的优先级,t2被设置了最低的优先级。t1不执行完,t2就没有机会执行。但由于t1在执行的中途休息了5秒中,这使得t2就有机会执行了。读者可以运行这个程序试试看。

  示例8:

public class ThreadTest implements Runnable
{
public synchronized void run()
{
for(int k=0;k<5;k++)
{
if(k == 2)
{
try
{
Thread.currentThread().sleep(5000);
}
catch(Exception e)
{}

}
System.out.println(Thread.currentThread().getName()
+ " : " + k);
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t1 = new Thread(r,"t1_name");
Thread t2 = new Thread(r,"t2_name");
t1.start();
t2.start();
}

  请读者首先运行示例8程序,从运行结果上看:一个线程在sleep的时候,并不会释放这个对象的锁标志。

  四:join()

  示例9:

public class ThreadTest implements Runnable
{
public static int a = 0;
public void run()
{
for(int k=0;k<5;k++)
{
a = a + 1;
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t = new Thread(r);
t.start();
System.out.println(a);
}

  请问程序的输出结果是5吗?答案是:有可能。其实你很难遇到输出5的时候,通常情况下都不是5。这里不讲解为什么输出结果不是5,我要讲的是:怎样才能让输出结果为5!其实很简单,join()方法提供了这种功能。join()方法,它能够使调用该方法的线程在此之前执行完毕。

  把示例9的main()方法该成如下这样:

public static void main(String[] args) throws Exception
{
Runnable r = new ThreadTest();
Thread t = new Thread(r);
t.start();
t.join();
System.out.println(a);

  这时,输出结果肯定是5!join()方法会抛出异常,应该提供捕获代码。或留给JDK捕获。

  示例10:

public class ThreadTest implements Runnable
{
public void run()
{
for(int k=0;k<10;k++)
{
System.out.print(" " + k);
}
}
public static void main(String[] args) throws Exception
{
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t1.join();
t2.start();
}

  运行这个程序,看看结果是否与示例3一样?

 五:yield()

  yield()方法与sleep()方法相似,只是它不能由用户指定线程暂停多长时间。按照SUN的说法:sleep方法可以使低优先级的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会。而yield()方法只能使同优先级的线程有执行的机会。

  示例11:

public class ThreadTest implements Runnable
{
public void run()
{
8
for(int k=0;k<10;k++)
{
if(k == 5 && Thread.currentThread().getName().equals("t1"))
{
Thread.yield();
}
System.out.println(Thread.currentThread().getName()
+ " : " + k);
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t1 = new Thread(r,"t1");
Thread t2 = new Thread(r,"t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
输出结果:
t1 : 0
t1 : 1
t1 : 2
t1 : 3
t1 : 4
t1 : 5
t1 : 6
t1 : 7
t1 : 8
t1 : 9
t2 : 0
t2 : 1
t2 : 2
t2 : 3
t2 : 4
t2 : 5
t2 : 6
t2 : 7
t2 : 8
t2 : 9 

  多次运行这个程序,输出也是一样。这说明:yield()方法不会使不同优先级的线程有执行的机会。

  六:wait(),notify(),notifyAll()

  首先说明:wait(),notify(),notifyAll()这些方法由java.lang.Object类提供,而上面讲到的方法都是由java.lang.Thread类提供(Thread类实现了Runnable接口)。

  wait(),notify(),notifyAll()这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用这三个方法。先看下面了例子:

  示例12:

public class ThreadTest implements Runnable
{
public static int shareVar = 0;
public synchronized void run()
{
if(shareVar == 0)
{
for(int i=0;i<10;i++)
{
shareVar++ ;
if(shareVar == 5)
{
try
{
this.wait();
}
catch(Exception e)
{}
}
}
}
if(shareVar != 0)
{
System.out.print(Thread.currentThread().getName());
System.out.println(" shareVar = " + shareVar);
this.notify();
}
}
public static void main(String[] args)
{
Runnable r = new ThreadTest();
Thread t1 = new Thread(r,"t1");
10
Thread t2 = new Thread(r,"t2");
t1.start();
t2.start();
}
}
运行结果:
t2 shareVar = 5
t1 shareVar = 10 

  t1线程最先执行。由于初始状态下shareVar为0,t1将使shareVar连续加1,当shareVar的值为5时,t1调用wait()方法,t1将处于休息状态,同时释放锁标志。这时t2得到了锁标志开始执行,shareVar的值已经变为5,所以t2直接输出shareVar的值,然后再调用notify()方法唤醒t1。t1接着上次休息前的进度继续执行,把shareVar的值一直加到10,由于此刻shareVar的值不为0,所以t1将输出此刻shareVar的值,然后再调用notify()方法,由于此刻已经没有等待锁标志的线程,所以此调用语句不起任何作用。

  这个程序简单的示范了wait(),notify()的用法,读者还需要在实践中继续摸索。

  七:关于线程的补充

  编写一个具有多线程能力的程序可以继承Thread类,也可以实现Runnable接口。在这两个方法中如何选择呢?从面向对象的角度考虑,作者建议你实现Runnable接口。有时你也必须实现Runnable接口,例如当你编写具有多线程能力的小应用程序的时候。

  线程的调度: NewRunningRunnableOtherwise BlockedDeadBlocked in object`sit()poolBlocked in object`slock poolnotify()Schedulercompletesrun()start()sleep() or join()sleep() timeout or thread join()s or interupt()Lockavailablesynchronized()Thread states

  terupt()一个Thread对象在它的生命周期中会处于各种不同的状态,上图形象地说明了这点。wa in


  调用start()方法使线程处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。

  实际上,程序中的多个线程并不是同时执行的。除非线程正在真正的多CPU计算机系统上执行,否则线程使用单CPU必须轮流执行。但是,由于这发生的很快,我们常常认为这些线程是同时执行的。

  JAVA运行时系统的计划调度程序是抢占性的。如果计划调度程序正在运行一个线程并且来了另一个优先级更高的线程,那么当前正在执行的线程就被暂时终止而让更高优先级的线程执行。

  JAVA计划调度程序不会为与当前线程具有同样优先级的另一个线程去抢占当前的线程。但是,尽管计划调度程序本身没有时间片(即它没有给相同优先级的线程以执行用的时间片),但以Thread类为基础的线程的系统实现可能会支持时间片分配。这依赖具体的操作系统,Windows与UNIX在这个问题上的支持不会完全一样。

  由于你不能肯定小应用程序将运行在什么操作系统上,因此你不应该编写出依赖时间片分配的程序。就是说,应该使用yield方法以允许相同优先级的线程有机会执行而不是希望每一个线程都自动得到一段CPU时间片。

  Thread类提供给你与系统无关的处理线程的机制。但是,线程的实际实现取决于JAVA运行所在的操作系统。因此,线程化的程序确实是利用了支持线程的操作系统。

  当创建线程时,可以赋予它优先级。它的优先级越高,它就越能影响运行系统。JAVA运行系统使用一个负责在所有执行JAVA程序内运行所有存在的计划调度程序。该计划调度程序实际上使用一个固定优先级的算法来保证每个程序中的最高优先级的线程得到CPU--允许最高优先级的线程在其它线程之前执行。

  对于在一个程序中有几个相同优先级的线程等待执行的情况,该计划调度程序循环地选择它们,当进行下一次选择时选择前面没有执行的线程,具有相同优先级的所有的线程都受到平等的对待。较低优先级的线程在较高优先级的线程已经死亡或者进入不可执行状态之后才能执行。

  继续讨论wait(),notify(),notifyAll():

  当线程执行了对一个特定对象的wait()调用时,那个线程被放到与那个对象相关的等待池中。此外,调用wait()的线程自动释放对象的锁标志。

  可以调用不同的wait():wait() 或wait(long timeout)

  对一个特定对象执行notify()调用时,将从对象的等待池中移走一个任意的线程,并放到锁标志等待池中,那里的线程一直在等待,直到可以获得对象的锁标志。notifyAll()方法将从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。只有锁标志等待池中的线程能获取对象的锁标志,锁标志允许线程从上次因调用wait()而中断的地方开始继续运行。

  在许多实现了wait()/notify()机制的系统中,醒来的线程必定是那个等待时间最长的线程。然而,在Java技术中,并不保证这点。

  注意,不管是否有线程在等待,都可以调用notify()。如果对一个对象调用notify()方法,而在这个对象的锁标志等待池中并没有线程,那么notify()调用将不起任何作用。

  在JAVA中,多线程是一个神奇的主题。之所以说它"神奇",是因为多线程程序的运行结果不可预测,但我们又可以通过某些方法控制多线程程序的执行。要想灵活使用多线程,读者还需要大量实践。

  另外,从JDK 1.2开始,SUN就不建议使用resume(),stop(),suspend()了。

2005/3/28

用Socket传输文件的test code

package server;

ThreadedEchoServer .java

====================================================

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 @author Cay Horstmann
 @version 1.20 2004-08-03
 */

/**
 * This program implements a multithreaded server that listens to port 8251 and
 * echoes back all client input.
 */
public class ThreadedEchoServer {
    static final int port=8251;
   
    public static void main(String[] args) {
        ServerSocket s=null;
        try {
            s = new ServerSocket(port);
            System.out.println("listening in port "+port+", ready for service...");
            while (true) {
                Socket incoming = s.accept();
                Runnable r = new ThreadedEchoHandler(incoming);
                Thread t = new Thread(r);
                t.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * This class handles the client input for one server socket connection.
 */

class ThreadedEchoHandler implements Runnable {

    private Socket incoming;

    public ThreadedEchoHandler(Socket i) {
        incoming = i;
    }

    public void run() {
        try {
            InputStream inStream = incoming.getInputStream();
            InputStream fileStream = new FileInputStream(new File("d:\\ttm.xls"));
            OutputStream outStream = incoming.getOutputStream();
            int lenRead = 0;
            int contentlength = 0;
            byte[] bBuf = new byte[65535];   
           
            while ((lenRead = fileStream.read(bBuf)) > 0) {
                contentlength = contentlength + lenRead;
                outStream.write(bBuf, 0, lenRead);
            }           
            BufferedReader in = new BufferedReader(new InputStreamReader(
                    inStream));
            System.out.println(in.readLine());
            in.close();
            inStream.close();
            fileStream.close();
            outStream.close();
           
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                incoming.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

SocketTest .java

====================================================

package client;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;

/**
 @version 1.20 2004-08-03
 @author Cay Horstmann
 */


/**
 * This program makes a socket connection to the atomic clock in Boulder,
 * Colorado, and prints the time that the server sends.
 */
public class SocketTest {
    static final int port=8251;
   
    public static void main(String[] args) {
        Socket s = null;
        long startTime = System.currentTimeMillis();
       
        try {

            s = new Socket("172.16.248.92", port);
            InputStream inStream = s.getInputStream();
            OutputStream outStream = s.getOutputStream();
            PrintWriter out = new PrintWriter(outStream, true /* autoFlush */);
            if (args.length > 0)
                out.println(args[0]);
            File fileto = new File("c:/","temp.xls");
            FileOutputStream fo = new FileOutputStream(fileto);
            int lenRead = 0;
            int contentlength = 0;
            byte[] bBuf = new byte[65535];   
           
            while ((lenRead = inStream.read(bBuf)) > 0) {
                contentlength = contentlength + lenRead;
                fo.write(bBuf, 0, lenRead);
            }
           
            out.close();
            inStream.close();
            outStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
         System.out.println("using time is :"+(System.currentTimeMillis()-startTime)/1000);
            try {
                s.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

序列化的示例代码

public class QueueItem implements Serializable {

    /**
     * Comment for <code>serialVersionUID</code>
     */
    private static final long serialVersionUID = 3689634687815266865L;
   
    private String name;
    /**
     *
     */
    public QueueItem() {
        super();
        // TODO Auto-generated constructor stub
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
   
    public void serialize(ObjectOutputStream oout) throws IOException{
        oout.writeObject(this);
    }

    public static QueueItem deserialize(ObjectInputStream oin) throws IOException, ClassNotFoundException{
        QueueItem s=(QueueItem)oin.readObject();
        return s;
    }

}

2005/1/28

简单的Timer例子

有时想要换一定的间隔时间执行重复的任务,java.util.Timer可以很简单的帮你实现。当
然你也可以用多线程来实现。下面是用java.util.Timer来实现这个功能。

//myTask.java  定义TimerTask任务,我们会在doTask中调用.

//这里只是简单的打印一下任务参数

public class myTask
    extends java.util.TimerTask {
  String jobName;
  private int i;
  public void run() { //run in interface Runnable
    System.out.println(jobName);
  }

  public myTask(String jobName) {
    this.jobName = jobName;
  }
}

//doTask.java

import java.util.*;
import java.io.*;

public class doTask {
  private java.util.Timer timer;
  private java.util.TimerTask task;
  public doTask(java.util.TimerTask task) {
    this.timer = new Timer();
    this.task = task;
  }
  public void start(int delay, int internal) {
    timer.schedule(task, delay * 1000, internal * 1000);//利用timer.schedule方法
  }

  public static void main(String[] args) {
    java.util.TimerTask task1 = new myTask("     Job 1");
    java.util.TimerTask task2= new myTask("Job 2");
    doTask pt = new doTask(task1);
    pt.start(1,3);
    doTask pt2 = new doTask(task2);
    pt2.start(1,1);
  }

}

结果如下;到每一个线程的输出与其它线程的输出相互交替


     Job 1
Job 2
Job 2
Job 2
     Job 1
Job 2
Job 2
Job 2
.
.