关于解决 Java 编程语言线程问题的建议(4)(转)[@more@]访问的问题
如果缺少良好的访问控制,会使线程编程非常困难。大多数情况下,如果能保证线程只从同步子系统中调用,不必考虑线程安全(threadsafe)问题。我建议对
Java 编程语言的访问权限概念做如下限制;
应精确使用 package
关键字来限制包访问权。我认为当缺省行为的存在是任何一种计算机语言的一个瑕疵,我对现在存在这种缺省权限感到很迷惑(而且这种缺省是“包(package)”级别的而不是“私有(private)”)。
在其它方面,Java 编程语言都不提供等同的缺省关键字。虽然使用显式的 package
的限定词会破坏现有代码,但是它将使代码的可读性更强,并能消除整个类的潜在错误
(例如,如果访问权是由于错误被忽略,而不是被故意忽略)。
重新引入 private protected,它的功能应和现在的 protected
一样,但是不应允许包级别的访问。
允许 private private 语法指定“实现的访问”对于所有外部对象是私有的,甚至是当前对象是的同一个类的。对于“.”左边的唯一引用(隐式或显式)应是
this。
扩展 public 的语法,以授权它可制定特定类的访问。例如,下面的代码应允许
Fred 类的对象可调用 some_method(),但是对其它类的对象,这个方法应是私有的。
public(Fred) void some_method()
{
}
这种建议不同于 C++ 的 "friend" 机制。 在 "friend" 机制中,它授权一个类访问另一个类的所有私有部分。在这里,我建议对有限的方法集合进行严格控制的访问。用这种方法,一个类可以为另一个类定义一个接口,而这个接口对系统的其余类是不可见的。一个明显的变化是:
public(Fred, Wilma) void some_method()
{
}
除非域引用的是真正不变(immutable)的对象或 static final 基本类型,否则所有域的定义应是
private。 对于一个类中域的直接访问违反了 OO 设计的两个基本规则:抽象和封装。从线程的观点来看,允许直接访问域只使对它进行非同步访问更容易一些。
增加 $property 关键字。带有此关键字的对象可被一个“bean
盒”应用程序访问,这个程序使用在 Class 类中定义的反射操作(introspection) API,否则与 private private 同效。 $property
属性可用在域和方法,这样现有的 JavaBean getter/setter 方法可以很容易地被定义为属性。
不变性(immutability)
由于对不变对象的访问不需要同步,所以在多线程条件下,不变的概念(一个对象的值在创建后不可更改)是无价的。Java
编程言语中,对于不变性的实现不够严格,有两个原因:
对于一个不变对象,在其被未完全创建之前,可以对它进行访问。这种访问对于某些域可以产生不正确的值。
对于恒定 (类的所有域都是 final) 的定义太松散。对于由
final 引用指定的对象,虽然引用本身不能改变,但是对象本身可以改变状态。
第一个问题可以解决,不允许线程在构造函数中开始执行
(或者在构造函数返回之前不能执行开始请求)。
对于第二个问题,通过限定 final 修饰符指向恒定对象,可以解决此问题。这就是说,对于一个对象,只有所有的域是
final,并且所有引用的对象的域也都是 final,此对象才真正是恒定的。为了不打破现有代码,这个定义可以使用编译器加强,即只有一个类被显式标为不变时,此类才是不变类。方法如下:
$immutable public class Fred
{
// all fields in this class must be final, and if the
// field is a reference, all fields in the referenced
// class must be final as well (recursively).
static int x constant = 0; // use of `final` is optional when $immutable
// is present.
}
有了 $immutable 修饰符后,在域定义中的 final 修饰符是可选的。
最后,当使用内部类(inner class)后,在 Java 编译器中的一个错误使它无法可靠地创建不变对象。当一个类有重要的内部类时(我的代码常有),编译器经常不正确地显示下列错误信息:
"Blank final variable ´name´ may not have been initialized.
It must be assigned a value in an initializer, or in every constructor."
既使空的 final 在每个构造函数中都有初始化,还是会出现这个错误信息。自从在
1.1 版本中引入内部类后,编译器中一直有这个错误。在此版本中(三年以后),这个错误依然存在。现在,该是改正这个错误的时候了。
对于类级域的实例级访问
除了访问权限外,还有一个问题,即类级(静态)方法和实例(非静态)方法都能直接访问类级(静态)域。这种访问是非常危险的,因为实例方法的同步不会获取类级的锁,所以一个
synchronized static 方法和一个 synchronized
方法还是能同时访问类的域。改正此问题的一个明显的方法是,要求在实例方法中只有使用
static 访问方法才能访问非不变类的 static
域。当然,这种要求需要编译器和运行时间检查。在这种规定下,下面的代码是非法的:
class Broken
{
static long x;
synchronized static void f()
{ x = 0;
}
synchronized void g()
{ x = -1;
}
};
由于 f() 和 g()
可以并行运行,所以它们能同时改变 x
的值(产生不定的结果)。请记住,这里有两个锁:static
方法要求属于 Class 对象的锁,而非静态方法要求属于此类实例的锁。当从实例方法中访问非不变 static 域时,编译器应要求满足下面两个结构中的任意一个:
class Broken
{
static long x;
synchronized private static accessor( long value )
{ x = value;
}
synchronized static void f()
{ x = 0;
}
synchronized void g()
{ accessor( -1 );
}
}
或则,编译器应获得读/写锁的使用:
class Broken
{
static long x;
synchronized static void f()
{ $writing(x){ x = 0 };
}
synchronized void g()
{ $writing(x){ x = -1 };
}
}
另外一种方法是(这也是一种理想的方法)-- 编译器应自动使用一个读/写锁来同步访问非不变 static 域,这样,程序员就不必担心这个问题。
后台线程的突然结束
当所有的非后台线程终止后,后台线程都被突然结束。当后台线程创建了一些全局资源(例如一个数据库连接或一个临时文件),而后台线程结束时这些资源没有被关闭或删除就会导致问题。
对于这个问题,我建议制定规则,使 Java 虚拟机在下列情况下不关闭应用程序:
有任何非后台线程正在运行,或者:
有任何后台线程正在执行一个 synchronized 方法或 synchronized 代码块。
后台线程在它执行完 synchronized 块或 synchronized 方法后可被立即关闭。
重新引入 stop()、
suspend() 和 resume()
关键字
由于实用原因这也许不可行,但是我希望不要废除 stop() (在 Thread 和 ThreadGroup 中)。但是,我会改变 stop()
的语义,使得调用它时不会破坏已有代码。但是,关于 stop() 的问题,请记住,当线程终止后,stop()
将释放所有锁,这样可能潜在地使正在此对象上工作的线程进入一种不稳定(局部修改)的状态。由于停止的线程已释放它在此对象上的所有锁,所以这些对象无法再被访问。
对于这个问题,可以重新定义 stop() 的行为,使线程只有在不占有任何锁时才立即终止。如果它占据着锁,我建议在此线程释放最后一个锁后才终止它。
可以使用一个和抛出异常相似的机制来实现此行为。被停止线程应设置一个标志,并且当退出所有同步块时立即测试此标志。如果设置了此标志,就抛出一个隐式的异常,
但是此异常应不再能被捕捉并且当线程结束时不会产生任何输出。注意,微软的 NT 操作系统不能很好地处理一个外部指示的突然停止(abrupt)。(它不把
stop 消息通知动态连接库,所以可能导致系统级的资源漏洞。)这就是我建议使用类似异常的方法简单地导致 run() 返回的原因。
与这种和异常类似的处理方法带来的实际问题是,你必需在每个 synchronized
块后都插入代码来测试“stopped”标志。并且这种附加的代码会降低系统性能并增加代码长度。我想到的另外一个办法是使 stop()
实现一个“延迟的(lazy)”停止,在这种情况下,在下次调用 wait()
或 yield() 时才终止。我还想向 Thread
中加入一个 isStopped() 和 stopped() 方法
(此时,Thread 将像isInterrupted() 和 interrupted()
一样工作,但是会检测 “stop-requested”的状态)。这种方法不向第一种那样通用,但是可行并且不会产生过载。
应把 suspend() 和 resume() 方法放回到 Java
编程语言中,它们是很有用的,我不想被当成是幼儿园的小孩。由于它们可能产生潜在的危险(当被挂起时,一个线程可以占据一个锁)而去掉它们是没有道理的。请让我自己来决定是否使用它们。
如果接收的线程正占据着锁,Sun 公司应该把它们作为调用 suspend() 的一个运行时间异常处理(run-time exception);或者更好的方法是,延迟实际的挂起过程,直到线程释放所有的锁。
被阻断的 I/O 应正确工作
应该能打断任何被阻断的操作,而不是只让它们 wait()
和 sleep()。我在“Taming Java Threads”的第二章中的 socket 部分讨论了此问题。但是现在,对于一个被阻断的 socket 上的
I/O 操作,打断它的唯一办法是关闭这个 socket,而没有办法打断一个被阻断的文件 I/O 操作。例如,一旦开始一个读请求并且进入阻断状态后,除非到它实际读出一些东西,
否则线程一直出于阻断状态。既使关掉文件句柄也不能打断读操作。
还有,程序应支持 I/O 操作的超时。所有可能出现阻断操作的对象(例如 InputStream 对象)也都应支持这种方法:
InputStream s = ...;
s.set_timeout( 1000 );
这和 Socket 类的 setSoTimeout(time)
方法是等价的。同样地,应该支持把超时作为参数传递到阻断的调用。
ThreadGroup 类
ThreadGroup 应该实现 Thread 中能够改变线程状态的所有方法。我特别想让它实现 join() 方法,这样我就可等待组中的所有线程的终止。
总结
以上是我的建议。就像我在标题中所说的那样,如果我是国王...(哎)。我希望这些改变(或其它等同的方法)最终能被引入
Java 语言中。我确实认为 Java 语言是一种伟大的编程语言;但是我也认为 Java 的线程模型设计得还不够完善,这是一件很可惜的事情。但是,Java
编程语言正在演变,所以还有可提高的前景。
参考资料
本文是对 Taming Java Threads 的更新摘编。该书
探讨了在 Java 语言中多线程编程的陷阱和问题,并提供了一个与线程相关的 Java 程序包来解决这些问题。
马里兰大学的 Bill Pugh 正在致力修改 JLS 来提高其线程模型。Bill 的提议并不如本文所推荐的那么广,他主要致力于让现有的线程模型以更为合理方式运行。更多信息可从 www.cs.umd.edu/~pugh/java/memoryModel/ 获得。
从 Sun 网站可找到全部 Java 语言的规范。
要从一个纯技术角度来审视线程,参阅 Doug Lea 编著的 Concurrent Programming in Java: Design Principles and Patterns 第二版。这是本很棒的书,但是它的风格是非常学术化的并不一定适合所有的读者。对《Taming Java Threads》是个很好的补充读物。
由 Scott Oaks 和 Henry Wong 编写的 Java Threads 比 Taming Java Threads 要轻量些,但是如果您从未编写过线程程序这本书更为适合。Oaks 和 Wong 同样实现了 Holub 提供的帮助类,而且看看对同一问题的不同解决方案总是有益的。
由 Bill Lewis 和 Daniel J. Berg 编写的 Threads Primer: A Guide to Multithreaded Programming 是对线程(不限于 Java)的很好入门介绍。
Java 线程的一些技术信息可在 Sun 网站上找到。
在 "Multiprocessor Safety and Java" 中 Paul Jakubik 讨论了多线程系统的 SMP 问题。
作者简介
Allen Holub 从 1979 年起就开始致力于计算机行业。他在各种杂志
(Dr. Dobb´s Journal、Programmers Journal、 Byte、MSJ 和其它杂志) 上发表了大量的文章。他为网络杂志
JavaWorld 撰写 “Java
工具箱”专栏,也为 IBM
developerWorks 组件技术专区 撰写“OO-设计流程”栏目。他还领导着
ITWorld 编程理论和实践讨论组。
Allen 撰写了八本书籍,最近新出的一本讨论了 Java 线程的陷阱和缺陷《Taming Java Threads》。他长期从事设计和编制面向对象软件。从事了
8 年的 C++ 编程工作后,Allen 在 1996 年由 C++ 转向 Java。他现在视 C++ 为一个噩梦,其可怕的经历正被逐渐淡忘。他从 1982 年起就自己和为加利弗尼亚大学伯克利分校教授计算机编程(首先是
C,然后是 C++ 和 MFC,现在是面向对象设计和 Java)。 Allen 也提供 Java 和面向对象设计方面的公众课程和私授 (in-house) 课程。他还提供面向对象设计的咨询并承包 Java 编程项目。请通过此 Web 站点和 Allen 取得联系并获取信息: www.holub.com。
如果缺少良好的访问控制,会使线程编程非常困难。大多数情况下,如果能保证线程只从同步子系统中调用,不必考虑线程安全(threadsafe)问题。我建议对
Java 编程语言的访问权限概念做如下限制;
应精确使用 package
关键字来限制包访问权。我认为当缺省行为的存在是任何一种计算机语言的一个瑕疵,我对现在存在这种缺省权限感到很迷惑(而且这种缺省是“包(package)”级别的而不是“私有(private)”)。
在其它方面,Java 编程语言都不提供等同的缺省关键字。虽然使用显式的 package
的限定词会破坏现有代码,但是它将使代码的可读性更强,并能消除整个类的潜在错误
(例如,如果访问权是由于错误被忽略,而不是被故意忽略)。
重新引入 private protected,它的功能应和现在的 protected
一样,但是不应允许包级别的访问。
允许 private private 语法指定“实现的访问”对于所有外部对象是私有的,甚至是当前对象是的同一个类的。对于“.”左边的唯一引用(隐式或显式)应是
this。
扩展 public 的语法,以授权它可制定特定类的访问。例如,下面的代码应允许
Fred 类的对象可调用 some_method(),但是对其它类的对象,这个方法应是私有的。
public(Fred) void some_method()
{
}
这种建议不同于 C++ 的 "friend" 机制。 在 "friend" 机制中,它授权一个类访问另一个类的所有私有部分。在这里,我建议对有限的方法集合进行严格控制的访问。用这种方法,一个类可以为另一个类定义一个接口,而这个接口对系统的其余类是不可见的。一个明显的变化是:
public(Fred, Wilma) void some_method()
{
}
除非域引用的是真正不变(immutable)的对象或 static final 基本类型,否则所有域的定义应是
private。 对于一个类中域的直接访问违反了 OO 设计的两个基本规则:抽象和封装。从线程的观点来看,允许直接访问域只使对它进行非同步访问更容易一些。
增加 $property 关键字。带有此关键字的对象可被一个“bean
盒”应用程序访问,这个程序使用在 Class 类中定义的反射操作(introspection) API,否则与 private private 同效。 $property
属性可用在域和方法,这样现有的 JavaBean getter/setter 方法可以很容易地被定义为属性。
不变性(immutability)
由于对不变对象的访问不需要同步,所以在多线程条件下,不变的概念(一个对象的值在创建后不可更改)是无价的。Java
编程言语中,对于不变性的实现不够严格,有两个原因:
对于一个不变对象,在其被未完全创建之前,可以对它进行访问。这种访问对于某些域可以产生不正确的值。
对于恒定 (类的所有域都是 final) 的定义太松散。对于由
final 引用指定的对象,虽然引用本身不能改变,但是对象本身可以改变状态。
第一个问题可以解决,不允许线程在构造函数中开始执行
(或者在构造函数返回之前不能执行开始请求)。
对于第二个问题,通过限定 final 修饰符指向恒定对象,可以解决此问题。这就是说,对于一个对象,只有所有的域是
final,并且所有引用的对象的域也都是 final,此对象才真正是恒定的。为了不打破现有代码,这个定义可以使用编译器加强,即只有一个类被显式标为不变时,此类才是不变类。方法如下:
$immutable public class Fred
{
// all fields in this class must be final, and if the
// field is a reference, all fields in the referenced
// class must be final as well (recursively).
static int x constant = 0; // use of `final` is optional when $immutable
// is present.
}
有了 $immutable 修饰符后,在域定义中的 final 修饰符是可选的。
最后,当使用内部类(inner class)后,在 Java 编译器中的一个错误使它无法可靠地创建不变对象。当一个类有重要的内部类时(我的代码常有),编译器经常不正确地显示下列错误信息:
"Blank final variable ´name´ may not have been initialized.
It must be assigned a value in an initializer, or in every constructor."
既使空的 final 在每个构造函数中都有初始化,还是会出现这个错误信息。自从在
1.1 版本中引入内部类后,编译器中一直有这个错误。在此版本中(三年以后),这个错误依然存在。现在,该是改正这个错误的时候了。
对于类级域的实例级访问
除了访问权限外,还有一个问题,即类级(静态)方法和实例(非静态)方法都能直接访问类级(静态)域。这种访问是非常危险的,因为实例方法的同步不会获取类级的锁,所以一个
synchronized static 方法和一个 synchronized
方法还是能同时访问类的域。改正此问题的一个明显的方法是,要求在实例方法中只有使用
static 访问方法才能访问非不变类的 static
域。当然,这种要求需要编译器和运行时间检查。在这种规定下,下面的代码是非法的:
class Broken
{
static long x;
synchronized static void f()
{ x = 0;
}
synchronized void g()
{ x = -1;
}
};
由于 f() 和 g()
可以并行运行,所以它们能同时改变 x
的值(产生不定的结果)。请记住,这里有两个锁:static
方法要求属于 Class 对象的锁,而非静态方法要求属于此类实例的锁。当从实例方法中访问非不变 static 域时,编译器应要求满足下面两个结构中的任意一个:
class Broken
{
static long x;
synchronized private static accessor( long value )
{ x = value;
}
synchronized static void f()
{ x = 0;
}
synchronized void g()
{ accessor( -1 );
}
}
或则,编译器应获得读/写锁的使用:
class Broken
{
static long x;
synchronized static void f()
{ $writing(x){ x = 0 };
}
synchronized void g()
{ $writing(x){ x = -1 };
}
}
另外一种方法是(这也是一种理想的方法)-- 编译器应自动使用一个读/写锁来同步访问非不变 static 域,这样,程序员就不必担心这个问题。
后台线程的突然结束
当所有的非后台线程终止后,后台线程都被突然结束。当后台线程创建了一些全局资源(例如一个数据库连接或一个临时文件),而后台线程结束时这些资源没有被关闭或删除就会导致问题。
对于这个问题,我建议制定规则,使 Java 虚拟机在下列情况下不关闭应用程序:
有任何非后台线程正在运行,或者:
有任何后台线程正在执行一个 synchronized 方法或 synchronized 代码块。
后台线程在它执行完 synchronized 块或 synchronized 方法后可被立即关闭。
重新引入 stop()、
suspend() 和 resume()
关键字
由于实用原因这也许不可行,但是我希望不要废除 stop() (在 Thread 和 ThreadGroup 中)。但是,我会改变 stop()
的语义,使得调用它时不会破坏已有代码。但是,关于 stop() 的问题,请记住,当线程终止后,stop()
将释放所有锁,这样可能潜在地使正在此对象上工作的线程进入一种不稳定(局部修改)的状态。由于停止的线程已释放它在此对象上的所有锁,所以这些对象无法再被访问。
对于这个问题,可以重新定义 stop() 的行为,使线程只有在不占有任何锁时才立即终止。如果它占据着锁,我建议在此线程释放最后一个锁后才终止它。
可以使用一个和抛出异常相似的机制来实现此行为。被停止线程应设置一个标志,并且当退出所有同步块时立即测试此标志。如果设置了此标志,就抛出一个隐式的异常,
但是此异常应不再能被捕捉并且当线程结束时不会产生任何输出。注意,微软的 NT 操作系统不能很好地处理一个外部指示的突然停止(abrupt)。(它不把
stop 消息通知动态连接库,所以可能导致系统级的资源漏洞。)这就是我建议使用类似异常的方法简单地导致 run() 返回的原因。
与这种和异常类似的处理方法带来的实际问题是,你必需在每个 synchronized
块后都插入代码来测试“stopped”标志。并且这种附加的代码会降低系统性能并增加代码长度。我想到的另外一个办法是使 stop()
实现一个“延迟的(lazy)”停止,在这种情况下,在下次调用 wait()
或 yield() 时才终止。我还想向 Thread
中加入一个 isStopped() 和 stopped() 方法
(此时,Thread 将像isInterrupted() 和 interrupted()
一样工作,但是会检测 “stop-requested”的状态)。这种方法不向第一种那样通用,但是可行并且不会产生过载。
应把 suspend() 和 resume() 方法放回到 Java
编程语言中,它们是很有用的,我不想被当成是幼儿园的小孩。由于它们可能产生潜在的危险(当被挂起时,一个线程可以占据一个锁)而去掉它们是没有道理的。请让我自己来决定是否使用它们。
如果接收的线程正占据着锁,Sun 公司应该把它们作为调用 suspend() 的一个运行时间异常处理(run-time exception);或者更好的方法是,延迟实际的挂起过程,直到线程释放所有的锁。
被阻断的 I/O 应正确工作
应该能打断任何被阻断的操作,而不是只让它们 wait()
和 sleep()。我在“Taming Java Threads”的第二章中的 socket 部分讨论了此问题。但是现在,对于一个被阻断的 socket 上的
I/O 操作,打断它的唯一办法是关闭这个 socket,而没有办法打断一个被阻断的文件 I/O 操作。例如,一旦开始一个读请求并且进入阻断状态后,除非到它实际读出一些东西,
否则线程一直出于阻断状态。既使关掉文件句柄也不能打断读操作。
还有,程序应支持 I/O 操作的超时。所有可能出现阻断操作的对象(例如 InputStream 对象)也都应支持这种方法:
InputStream s = ...;
s.set_timeout( 1000 );
这和 Socket 类的 setSoTimeout(time)
方法是等价的。同样地,应该支持把超时作为参数传递到阻断的调用。
ThreadGroup 类
ThreadGroup 应该实现 Thread 中能够改变线程状态的所有方法。我特别想让它实现 join() 方法,这样我就可等待组中的所有线程的终止。
总结
以上是我的建议。就像我在标题中所说的那样,如果我是国王...(哎)。我希望这些改变(或其它等同的方法)最终能被引入
Java 语言中。我确实认为 Java 语言是一种伟大的编程语言;但是我也认为 Java 的线程模型设计得还不够完善,这是一件很可惜的事情。但是,Java
编程语言正在演变,所以还有可提高的前景。
参考资料
本文是对 Taming Java Threads 的更新摘编。该书
探讨了在 Java 语言中多线程编程的陷阱和问题,并提供了一个与线程相关的 Java 程序包来解决这些问题。
马里兰大学的 Bill Pugh 正在致力修改 JLS 来提高其线程模型。Bill 的提议并不如本文所推荐的那么广,他主要致力于让现有的线程模型以更为合理方式运行。更多信息可从 www.cs.umd.edu/~pugh/java/memoryModel/ 获得。
从 Sun 网站可找到全部 Java 语言的规范。
要从一个纯技术角度来审视线程,参阅 Doug Lea 编著的 Concurrent Programming in Java: Design Principles and Patterns 第二版。这是本很棒的书,但是它的风格是非常学术化的并不一定适合所有的读者。对《Taming Java Threads》是个很好的补充读物。
由 Scott Oaks 和 Henry Wong 编写的 Java Threads 比 Taming Java Threads 要轻量些,但是如果您从未编写过线程程序这本书更为适合。Oaks 和 Wong 同样实现了 Holub 提供的帮助类,而且看看对同一问题的不同解决方案总是有益的。
由 Bill Lewis 和 Daniel J. Berg 编写的 Threads Primer: A Guide to Multithreaded Programming 是对线程(不限于 Java)的很好入门介绍。
Java 线程的一些技术信息可在 Sun 网站上找到。
在 "Multiprocessor Safety and Java" 中 Paul Jakubik 讨论了多线程系统的 SMP 问题。
作者简介
Allen Holub 从 1979 年起就开始致力于计算机行业。他在各种杂志
(Dr. Dobb´s Journal、Programmers Journal、 Byte、MSJ 和其它杂志) 上发表了大量的文章。他为网络杂志
JavaWorld 撰写 “Java
工具箱”专栏,也为 IBM
developerWorks 组件技术专区 撰写“OO-设计流程”栏目。他还领导着
ITWorld 编程理论和实践讨论组。
Allen 撰写了八本书籍,最近新出的一本讨论了 Java 线程的陷阱和缺陷《Taming Java Threads》。他长期从事设计和编制面向对象软件。从事了
8 年的 C++ 编程工作后,Allen 在 1996 年由 C++ 转向 Java。他现在视 C++ 为一个噩梦,其可怕的经历正被逐渐淡忘。他从 1982 年起就自己和为加利弗尼亚大学伯克利分校教授计算机编程(首先是
C,然后是 C++ 和 MFC,现在是面向对象设计和 Java)。 Allen 也提供 Java 和面向对象设计方面的公众课程和私授 (in-house) 课程。他还提供面向对象设计的咨询并承包 Java 编程项目。请通过此 Web 站点和 Allen 取得联系并获取信息: www.holub.com。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10617731/viewspace-958381/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/10617731/viewspace-958381/