多线程是较复杂程序设计过程中不可缺少的一部分为了提高应用程序运行的性能采用多线程的设计是一种比较可行的方案本文通过介绍使用Java编写的扫描计算机端口的实例来说明多线程设计中应注意的问题以及得出经常使用的多线程模型
本文要求读者具备一定的Java语言基础对Socket有一定的了解本文的所有程序在JavaSDK编译通过并能正常运行
现在我们需要对一台主机扫描其端口找出哪些端口是open的状态我们先采用单线程进行处理程序代码如下
importjavaioIOException;
importSocket;
importUnknownHostException;
publicclassPortScannerSingleThread{
publicstaticvoidmain(String[]args){
Stringhost=null;//第一个参数目标主机
intbeginport=;//第二个参数开始端口
intendport=;//第三个参数结束端口
try{
host=args[];
beginport=IntegerparseInt(args[]);
endport=IntegerparseInt(args[]);
if(beginport<=||endport>=||beginport>endport){
thrownewException(Portisillegal);
}
}catch(Exceptione){
Systemoutprintln(Usage:javaPortScannerSingleThreadhostbeginportendport);
Systemexit();
}
for(inti=beginport;i<=endport;i++){
try{
Sockets=newSocket(hosti);
Systemoutprintln(Theport+i+isopenedat+host);
}catch(UnknownHostExceptionex){
Systemerrprintln(ex);
break;
}catch(IOExceptionex){
}
}
}
}
在以上程序中通过Socket类来识别端口是否是open状态程序接受个参数第一个参数是主机IP第二和第三个参数是需要扫描的起始和中止的端口号(~)本程序(javaPortScannerSingleThread)运行结果如下
Theportisopenedat
Theportisopenedat
Theportisopenedat
但是以上程序运行效率实在不敢恭维把目标主机端口扫描一遍需要十几分钟甚至更长估计没有哪个用户可以忍受这样的效率
所以提高程序处理效率是必须的下面的程序通过多线程的方法来进行处理程序代码如下
importjavaioIOException;
importSocket;
importUnknownHostException;
publicclassPortScannerMultiThread{
publicstaticvoidmain(String[]args){
Stringhost=null;
intbeginport=;
intendport=;
try{
host=args[];
beginport=IntegerparseInt(args[]);
endport=IntegerparseInt(args[]);
if(beginport<=||endport>=||beginport>endport){
thrownewException(Portisillegal);
}
}catch(Exceptione){
Systemoutprintln(Usage:javaPortScannerSingleThreadhostbeginportendport);
Systemexit();
}
for(inti=beginport;i<=endport;i++){
PortProcessorpp=newPortProcessor(hosti);//一个端口创建一个线程
ppstart();
}
}
}
classPortProcessorextendsThread{
Stringhost;
intport;
PortProcessor(Stringhostintport){
thishost=host;
thisport=port;
}
publicvoidrun(){
try{
Sockets=newSocket(hostport);
Systemoutprintln(Theport+port+isopenedat+host);
}catch(UnknownHostExceptionex){
Systemerrprintln(ex);
}catch(IOExceptionioe){
}
}
}
以上程序在for循环结构中创建PortProcessor对象PortProcessor类是线程类其关键的Socket在publicvoidrun()方法中实现此程序比第一个单线程的程序运行效率提高很多倍几乎在几秒钟内得出结果所以可见多线程处理是何等的重要
程序(javaPortScannerMultiThread)运行结果如下
Theportisopenedat
Theportisopenedat
Theportisopenedat
仔细对第个程序分析不难发现其中的问题创建的线程个数是不固定的取决于输入的第二和第三个参数如果扫描~端口那么主线程就产生个线程来分别处理如果扫描~端口主线程就会产生个线程来进行处理在JVM中创建如此多的线程同样会带来性能上的问题因为线程的创建和消失都是需要花费系统资源的所以以上的第二个程序也存在明显的不足
所以我们需要一个确定数量的线程在JVM中运行这样就需要了解线程池(ThreadPool)的概念线程池在多线程程序设计中是比不可少的而且初学者不太容易掌握下面通过对线程池的介绍结合第和第个程序引出两种常用的线程池模型
第一种实现线程池的方法是创建一个池在池中增加要处理的数据对象然后创建一定数量的线程这些线程对池中的对象进行处理当池是空的时候每个线程处于等待状态当往池里添加一个对象通知所有等待的线程来处理(当然一个对象只能有一个线程来处理)
第二种方法是同样创建一个池但是在池中放的不是数据对象而是线程可以把池中的一个个线程比喻成一个个工人当没有任务的时候工人们严阵以待当给池添加一个任务后工人就开始处理并直到处理完成
在第个程序中定义了List类型的entries作为池这个池用来保存需要扫描的端口List中的元素必须是Object类型不能用基本数据类型int往池里添加而需要用使用Integer在processMethod()方法中首先就启动一定数量的PortThread线程同时在while循环中通过entriesadd(newInteger(port))往池里添加对象在PortThread类的run()方法中通过entry=(Integer)entriesremove(entriessize());取得池中的对象转换成int后传递给Socket构造方法
第个程序如下
importjavaioIOException;
importInetAddress;
importSocket;
importUnknownHostException;
importjavautilCollections;
importjavautilLinkedList;
importjavautilList;
publicclassPortScanner{
privateListentries=CollectionssynchronizedList(newLinkedList());//这个池比较特别
intnumofthreads;
staticintport;
intbeginport;
intendport;
InetAddressremote=null;
publicbooleanisFinished(){
if(port>=endport){
returntrue;
}else{
returnfalse;
}
}
PortScanner(InetAddressaddrintbeginportintendportintnumofthreads){
thisremote=addr;
thisbeginport=beginport;
thisendport=endport;
thisnumofthreads=numofthreads;
}
publicvoidprocessMethod(){
for(inti=;i<numofthreads;i++){//创建一定数量的线程并运行
Threadt=newPortThread(remoteentriesthis);
tstart();
}
port=beginport;
while(true){
if(entriessize()>numofthreads){
try{
Threadsleep();//池中的内容太多的话就sleep
}catch(InterruptedExceptionex){
}
continue;
}
synchronized(entries){
if(port>endport)break;
entriesadd(newInteger(port));//往池里添加对象需要使用int对应的Integer类
entriesnotifyAll();
port++;
}
}
}
publicstaticvoidmain(String[]args){
Stringhost=null;
intbeginport=;
intendport=;
intnThreads=;
try{
host=args[];
beginport=IntegerparseInt(args[]);
endport=IntegerparseInt(args[]);
nThreads=IntegerparseInt(args[]);
if(beginport<=||endport>=||beginport>endport){
thrownewException(Portisillegal);
}
}catch(Exceptione){
Systemoutprintln(Usage:javaPortScannerSingleThreadhostbeginportendportnThreads);
Systemexit();
}
try{
PortScannerscanner=newPortScanner(InetAddressgetByName(host)beginportendportnThreads);
scannerprocessMethod();
}catch(UnknownHostExceptionex){
}
}
}
classPortThreadextendsThread{
privateInetAddressremote;
privateListentries;
PortScannerscanner;
PortThread(InetAddressaddListentriesPortScannerscanner){
thisremote=add;
thisentries=entries;
thisscanner=scanner;
}
publicvoidrun(){
Integerentry;
while(true){
synchronized(entries){
while(entriessize()==){
if(scannerisFinished())return;
try{
entrieswait();//池里没内容就只能等了
}catch(InterruptedExceptionex){
}
}
entry=(Integer)entriesremove(entriessize());//把池里的东西拿出来进行处理
}
Sockets=null;
try{
s=newSocket(remoteentryintValue());
Systemoutprintln(Theportof+entrytoString()+oftheremote+remote+isopened);
}catch(IOExceptione){
}finally{
try{
if(s!=null)sclose();
}catch(IOExceptione){
}
}
}
}
}
以上程序需要个参数输入javaPortScanner运行(第个参数是线程数)结果前两个程序一样但是速度比第一个要快可能比第二个要慢一些
第个程序是把端口作为池中的对象下面我们看第个实现方式把池里面的对象定义成是线程类把具体的任务定义成池中线程类的参数第个程序有个文件组成分别是ThreadPooljava和PortScannerByThreadPooljava
ThreadPooljava文件内容如下
importjavautilLinkedList;
publicclassThreadPool{
privatefinalintnThreads;
privatefinalPoolWorker[]threads;
privatefinalLinkedListqueue;
publicThreadPool(intnThreads){
thisnThreads=nThreads;
queue=newLinkedList();
threads=newPoolWorker[nThreads];
for(inti=;i<nThreads;i++){
threads[i]=newPoolWorker();
threads[i]start();
}
}
publicvoidexecute(Runnabler){
synchronized(queue){
queueaddLast(r);
queuenotifyAll();
}
}
privateclassPoolWorkerextendsThread{
publicvoidrun(){
Runnabler;
while(true){
synchronized(queue){
while(queueisEmpty()){
try{
queuewait();
}catch(InterruptedExceptionignored){
}
}
r=(Runnable)queueremoveFirst();
}
try{
rrun();
}
catch(RuntimeExceptione){
}
}
}
}
}
在ThreadPooljava文件中定义了个类ThreadPool和PoolWorkerThreadPool类中的nThreads变量表示线程数PoolWorker数组类型的threads变量表示线程池中的工人这些工人的工作就是一直循环处理通过queueaddLast(r)加入到池中的任务
PortScannerByThreadPooljava文件内容如下
importjavaioIOException;
importInetAddress;
importSocket;
publicclassPortScannerByThreadPool{
publicstaticvoidmain(String[]args){
Stringhost=null;
intbeginport=;
intendport=;
intnThreads=;
try{
host=args[];
beginport=IntegerparseInt(args[]);
endport=IntegerparseInt(args[]);
nThreads=IntegerparseInt(args[]);
if(beginport<=||endport>=||beginport>endport){
thrownewException(Portisillegal);
}
}catch(Exceptione){
Systemoutprintln(Usage:javaPortScannerSingleThreadhostbeginportendportnThreads);
Systemexit();
}
ThreadPooltp=newThreadPool(nThreads);
for(inti=beginport;i<=endport;i++){
Scannerps=newScanner(hosti);
tpexecute(ps);
}
}
}
classScannerimplementsRunnable{
Stringhost;
intport;
Scanner(Stringhostintport){
thishost=host;
thisport=port;
}
publicvoidrun(){
Sockets=null;
try{
s=newSocket(InetAddressgetByName(host)port);
Systemoutprintln(Theportof+port+isopened);
}catch(IOExceptionex){
}finally{
try{
if(s!=null)sclose();
}catch(IOExceptione){
}
}
}
}
PortScannerByThreadPool是主程序类处理输入的个参数(和第个程序是一样的)主机名开始端口结束端口和线程数Scanner类定义了真正的任务在PortScannerByThreadPool中通过newThreadPool(nThreads)创建ThreadPool对象然后在for循环中通过newScanner(hosti)创建任务对象再通过tpexecute(ps)把任务对象添加到池中
读者可以编译运行第个程序得出的结果和前面的是一样的但是第和第个程序之间最大的差别就是第个程序会一直运行下去不会自动结束在第个程序中存在一个isFinished()方法可以用来判断任务是否处理完毕而第个程序中没有这样做请读者自己思考这个问题
在第和第个程序中我们可以概括出多线程的模型第个程序的线程池里装的要处理的对象第个程序的线程池里装的是工人还需要通过定义任务并给把它派工给工人我个人比较偏好后者的线程池模型虽然类的个数多了几个但逻辑很清晰不管怎样第和第个程序中关键的部分都大同小异就是个synchronized程序块中的内容如下(第个程序中的)
synchronized(queue){
queueaddLast(r);
queuenotifyAll();
}
和
synchronized(queue){
while(queueisEmpty()){
try{
queuewait();
}catch(InterruptedExceptionignored){
}
}
r=(Runnable)queueremoveFirst();
}
一般拿synchronized用来定义方法或程序块这样可以在多线程同时访问的情况下保证在一个时刻只能有一个线程对这部分内容进行访问避免了数据出错在第个程序中通过Listentries=CollectionssynchronizedList(newLinkedList())来定义池在第个程序中直接用LinkedListqueue都差不多只是CollectionssynchronizedList()可以保证池的同步其实池里的内容访问都是在synchronized定义的程序块中所以不用CollectionssynchronizedList()也是可以的
wait()和notifyAll()是很重要的而且这个方法是Object基类的方法所以任何一个类都是可以使用的这里说明一个可能产生混淆的问题queuewait()并不是说queue对象需要进行等待而是说queuewait()所在的线程需要进行等待并且释放对queue的锁把对queue的访问权交给别的线程如果读者对这个方法难以理解建议参考JDK的文档说明
好了通过以上个例子的理解读者应该能对多线程的程序设计有了一定的理解第和第个程序对应线程模型是非常重要的可以说是多线程程序设计过程中不可或缺的内容
如果读者对以上的内容有任何疑问可以和我联系版权所有严禁转载
参考资料
《JavaNetworkingProgrammingrd》writtenbyElliotteRustyHaroldPublishedbyOReilly
ThreadpoolsandworkqueueswrittenbyBrianGoetzPrincipalConsultantQuiotixCorp