多线程程序模型之线程池

    |     2015年4月10日   |   多线程编程   |     0 条评论   |    1645

多线程是复杂程序里几乎躲不开的一块。为了把运行性能拉上来,把工作拆成多条线程是常见做法。下面用扫描主机端口的例子,把单线程、一端口一线程、以及两种线程池模型都走一遍。

阅读前提:有一定 Java 基础,对 Socket 不陌生。原文程序在 Java SDK 1.4.2 下编译运行通过。

一、单线程扫端口

任务:扫一台主机,找出哪些端口是 open。先用单线程,靠 java.net.Socket 判断端口是否打开。三个参数:主机 IP、起始端口、结束端口(1~65535)。

import java.io.IOException;

import java.net.Socket;

import java.net.UnknownHostException;



public class PortScannerSingleThread {

    public static void main(String[] args) {

        String host = null;        //第一个参数,目标主机。

        int beginport = 1;         //第二个参数,开始端口。

        int endport = 65535;       //第三个参数,结束端口。

        try{

            host = args[0];

            beginport = Integer.parseInt(args[1]);

            endport = Integer.parseInt(args[2]);

            if(beginport <= 0 || endport >= 65536 || beginport > endport){

                throw new Exception("Port is illegal");

            }

        }catch(Exception e){

            System.out.println("Usage: java PortScannerSingleThread host beginport endport");

            System.exit(0);

        }

        



        for (int i = beginport; i <= endport; i++) {

            try {

                Socket s = new Socket(host, i);

                System.out.println("The port " + i + " is opened at " + host);

            }catch (UnknownHostException ex) {

                System.err.println(ex);

                break;

            }catch (IOException ex) {

            }

        }

    }

}

运行 java PortScannerSingleThread 10.1.1.1 1 1000 会看到类似:

The port 25 is opened at 10.1.1.182
The port 110 is opened at 10.1.1.182
The port 135 is opened at 10.1.1.182
...

把目标端口扫一遍要十几分钟甚至更久,这种效率没人能忍。必须提速。

二、一端口一线程

for 循环里为每个端口 new 一个 PortProcessor 线程,真正的 Socket 放在 run() 里:

import java.io.IOException;

import java.net.Socket;

import java.net.UnknownHostException;



public class PortScannerMultiThread {

    public static void main(String[] args) {

        String host = null;

        int beginport = 1;

        int endport = 65535;

        try{

            host = args[0];

            beginport = Integer.parseInt(args[1]);

            endport = Integer.parseInt(args[2]);

            if(beginport <= 0 || endport >= 65536 || beginport > endport){

                throw new Exception("Port is illegal");

            }

        }catch(Exception e){

            System.out.println("Usage: java PortScannerSingleThread host beginport endport");

            System.exit(0);

        }

        



        for (int i = beginport; i <= endport; i++) {

            PortProcessor pp = new PortProcessor(host,i);      //一个端口创建一个线程

            pp.start();

        }

    }

}



class PortProcessor extends Thread{

    String host;

    int port;

    

    PortProcessor(String host, int port){

        this.host = host;

        this.port = port;

    }

    

    public void run(){

        try{

            Socket s = new Socket(host,port);

            System.out.println("The port " + port + " is opened at " + host);

        }catch(UnknownHostException ex){

            System.err.println(ex);

        }catch(IOException ioe){

        }

    }

}

比单线程快很多倍,扫 1~1000 几乎几秒出结果。运行 java PortScannerMultiThread 10.1.1.100 1 1000:

The port 25 is opened at 10.1.1.100
The port 42 is opened at 10.1.1.100
The port 88 is opened at 10.1.1.100
...

问题也很明显:线程数取决于起止端口。扫 1~100 就 100 条线程,扫 1~10000 就一万条。JVM 里创建、销毁线程本身就吃资源,所以这个版本也不够用。我们需要数量确定的线程,也就是线程池(ThreadPool)。

三、两种池模型

第一种:池里放待处理数据。先建池,往里丢对象,再启动固定数量的线程去取。池空则线程 wait;往池里加一个对象后 notifyAll,一个对象只允许一个线程处理。

第二种:池里放工人线程。没事时工人待命;往池里丢一个任务,工人就领走做到完。

模型 池里装什么 对应程序 结束方式
任务池 要扫的端口(Integer) 程序 3 PortScanner isFinished() 后线程 return
工人池 PoolWorker,任务走 execute(Runnable) 程序 4 ThreadPool + Scanner 没有结束判断,会一直跑

四、程序 3:池里装端口

List entries 当池,装待扫端口。List 元素必须是 Object,不能丢裸 int,要用 Integer。processMethod() 先启动固定数量的 PortThread,再在 while 里 entries.add(0, new Integer(port))。线程的 run() 用 entries.remove(entries.size()-1) 取出,转 int 再交给 Socket。

import java.io.IOException;

import java.net.InetAddress;

import java.net.Socket;

import java.net.UnknownHostException;

import java.util.Collections;

import java.util.LinkedList;

import java.util.List;



public class PortScanner {

    private List entries = Collections.synchronizedList(new LinkedList());  //这个”池“比较特别

    int numofthreads;

    static int port;

    int beginport;

    int endport;

    InetAddress remote = null;

    

    public boolean isFinished(){

        if(port >= endport){

            return true;

        }else{

            return false;

        }

    }

    

    PortScanner(InetAddress addr, int beginport, int endport, int numofthreads){

        this.remote = addr;

        this.beginport = beginport;

        this.endport = endport;

        this.numofthreads = numofthreads;    

    }

    

    public void processMethod(){

        for(int i = 0; i < numofthreads; i++){          //创建一定数量的线程并运行

            Thread t = new PortThread(remote, entries, this);

            t.start();

        }

        

        port = beginport;

        

        while(true){

            if(entries.size() > numofthreads){

                try{

                    Thread.sleep(1000);      //”池“中的内容太多的话就sleep

                }catch(InterruptedException ex){

                    

                }

                continue;

            }

            

            synchronized(entries){

                if(port > endport) break;

                entries.add(0, new Integer(port));  //往”池“里添加对象,需要使用int对应的Integer类

                entries.notifyAll();

                port++;

            }

        }

    }

    

    public static void main(String[] args) {

        String host = null;

        int beginport = 1;

        int endport = 65535;

        int nThreads = 100;

        try{

            host = args[0];

            beginport = Integer.parseInt(args[1]);

            endport = Integer.parseInt(args[2]);

            nThreads = Integer.parseInt(args[3]);

            if(beginport <= 0 || endport >= 65536 || beginport > endport){

                throw new Exception("Port is illegal");

            }

        }catch(Exception e){

            System.out.println("Usage: java PortScannerSingleThread host beginport endport nThreads");

            System.exit(0);

        }

        

        try{

            PortScanner scanner = new PortScanner(InetAddress.getByName(host), beginport, endport, nThreads);

            scanner.processMethod();

        }catch(UnknownHostException ex){

        }    

    }

}



class PortThread extends Thread{

    private InetAddress remote;

    private List entries;

    PortScanner scanner;

    

    PortThread(InetAddress add, List entries, PortScanner scanner){

        this.remote = add;

        this.entries = entries;

        this.scanner = scanner;

    }

            

    public void run(){

        Integer entry;

        while(true){

            synchronized(entries){

                while(entries.size() == 0){

                    if(scanner.isFinished()) return;

                    try{

                        entries.wait();           //”池“里没内容就只能等了

                    }catch(InterruptedException ex){

                    }

                }

                entry = (Integer)entries.remove(entries.size()-1);  //把”池“里的东西拿出来进行处理

            }

            

            Socket s = null;

            

            try{

                s = new Socket(remote, entry.intValue());

                System.out.println("The port of " + entry.toString() + " of the remote " + remote +" is opened.");

            

            }catch(IOException e){

            }finally{

                try{

                    if(s != null) s.close();

                }catch(IOException e){

                    

                }

            }

        }

    }

}

四个参数。运行 java PortScanner 10.1.1.182 1 10000 100(第四个是线程数),结果和前两个程序一样:比程序 1 快,可能比程序 2 稍慢。

五、程序 4:池里装工人

池里的对象改成线程(工人),具体任务作为参数交给工人。两个文件:ThreadPool.java 和 PortScannerByThreadPool.java。

ThreadPool 里 nThreads 是工人数,PoolWorker[] 是工人数组。工人的工作就是循环处理通过 queue.addLast(r) 丢进池里的任务:

import java.util.LinkedList;



public class ThreadPool{

    private final int nThreads;

    private final PoolWorker[] threads;

    private final LinkedList queue;



    public ThreadPool(int nThreads){

        this.nThreads = nThreads;

        queue = new LinkedList();

        threads = new PoolWorker[nThreads];



        for (int i=0; i<nThreads; i++) {

            threads[i] = new PoolWorker();

            threads[i].start();

        }

    }



    public void execute(Runnable r) {

        synchronized(queue) {

            queue.addLast(r);

            queue.notifyAll();

        }

    }



    private class PoolWorker extends Thread {

        public void run() {

            Runnable r;



            while (true) {

                synchronized(queue) {

                    while (queue.isEmpty()) {

                        try{

                            queue.wait();

                        }catch (InterruptedException ignored){

                        }

                    }



                    r = (Runnable) queue.removeFirst();

                }



                try {

                    r.run();

                }

                catch (RuntimeException e) {

                }

            }

        }

    }

}

主程序同样吃四个参数:主机、起止端口、线程数。Scanner 才是真正的任务;new ThreadPool(nThreads) 之后,for 里 new Scanner(host, i) 再 tp.execute(ps) 把任务派给工人:

import java.io.IOException;

import java.net.InetAddress;

import java.net.Socket;



public class PortScannerByThreadPool {

    public static void main(String[] args) {

        String host = null;

        int beginport = 1;

        int endport = 65535;

        int nThreads = 100;

        try{

            host = args[0];

            beginport = Integer.parseInt(args[1]);

            endport = Integer.parseInt(args[2]);

            nThreads = Integer.parseInt(args[3]);

            if(beginport <= 0 || endport >= 65536 || beginport > endport){

                throw new Exception("Port is illegal");

            }

        }catch(Exception e){

            System.out.println("Usage: java PortScannerSingleThread host beginport endport nThreads");

            System.exit(0);

        }

        

        ThreadPool tp = new ThreadPool(nThreads);

        

        for(int i = beginport; i <= endport; i++){

            Scanner ps = new Scanner(host,i);

            tp.execute(ps);

        }

    }

}

    

class Scanner implements Runnable{

    String host;    

    int port;

        

    Scanner(String host, int port){

        this.host = host;

        this.port = port;

    }

        

    public void run(){

        Socket s = null;

        try{

            s = new Socket(InetAddress.getByName(host),port);

            System.out.println("The port of " + port + " is opened.");

        }catch(IOException ex){

        }finally{

            try{

                if(s != null) s.close();

            }catch(IOException e){

            }

        }

    }

}

结果和前面相同。最大差别:程序 4 会一直运行、不会自动结束。程序 3 有 isFinished() 判断任务是否做完,程序 4 没有——这一点留给读者想。

六、wait / notify 才是池的心脏

两种模型关键段落几乎一样:都是两段 synchronized。程序 4 里是:

synchronized(queue) {

    queue.addLast(r);

    queue.notifyAll();

}

和

synchronized(queue) {

    while (queue.isEmpty()) {

        try{

            queue.wait();

        }catch (InterruptedException ignored){

        }

    }



    r = (Runnable) queue.removeFirst();

}

synchronized 用来修饰方法或代码块,保证同一时刻只有一条线程进入,避免数据写乱。程序 3 用 Collections.synchronizedList(new LinkedList()) 包一层;程序 4 直接用 LinkedList queue。池里的访问已经在 synchronized 块里,不用 synchronizedList 也可以。

wait() 和 notifyAll() 是 Object 的方法,任何类都能调。容易混的一点:queue.wait() 不是让 queue 对象去等,而是让当前线程等,并释放对 queue 的锁,把访问权交给别人。细节见 JDK 文档。

程序 3 的池装待处理对象,程序 4 的池装工人、再把任务派工。个人更偏好后者:类多几个,但逻辑清楚。这两种模型是多线程设计里几乎躲不开的内容。

一句话总结:不要一端口一线程:固定数量的工人 + wait/notify 的任务队列,才是能落地的线程池。

转载请注明来源:多线程程序模型之线程池
本文链接地址:https://ai.zhousir.top/?p=153
回复 取消