1.4 多线程的优点

2016-05-29 14:51:17 6,716 4


尽管面临很多挑战,多线程有一些优点使得它一直被使用。这些优点是:

* 资源利用率更好

* 程序设计在某些情况下更简单

* 程序响应更快

资源利用率更好

想象一下,一个应用程序需要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件需要5秒,处理一个文件需要2秒。

5秒读取文件A
2秒处理文件A
5秒读取文件B
2秒处理文件B
---------------------
总共需要14秒

上述情况可以用以下的代码进行模拟。

public class ResourceDemo {
    public static void main(String[] args) throws InterruptedException {
        final long start=System.currentTimeMillis();
        System.out.println("----------程序开始运行---------");
        System.out.println("读取A文件开始...");
        Thread.currentThread().sleep(5000);
        System.out.println("读取A文件结束,耗时:"+(System.currentTimeMillis()-start)/1000+"秒...开始处理A文件");
        Thread.currentThread().sleep(2000);
        System.out.println("A文件处理完成...,耗时:"+(System.currentTimeMillis()-start)/1000+"秒");
        System.out.println("读取B文件开始...");
        Thread.currentThread().sleep(5000);
        System.out.println("读取B文件结束,耗时:"+(System.currentTimeMillis()-start)/1000+"秒...开始处理B文件");
        Thread.currentThread().sleep(2000);
        System.out.println("B文件处理完成...,耗时:"+(System.currentTimeMillis()-start)/1000+"秒");
    }
}

这段代码我们使用了一个方法Thread.currentThread().sleep(miliseconds)来模拟文件的模拟和处理操作。其作用是让当前线程休眠,休眠的含义是在指定的时间范围内,线程不会再向CPU发送执行的请求。等到休眠时间已过,才会重新请求CPU执行。因为我们的代码都是在main方法即主线程中运行,因此当主线程休眠的时候,就相当于程序停止了运行,等到休眠时间已过,程序才会继续运行,然后又休眠,运行...。

程序运行的结果如下:

----------程序开始运行---------

读取A文件开始...

读取A文件结束,耗时:5秒...开始处理A文件

A文件处理完成...,耗时:7秒

读取B文件开始...

读取B文件结束,耗时:12秒...开始处理B文件

B文件处理完成...,耗时:14秒


需要注意的是,上面的代码,资源利用率是很低的。

原因在于从磁盘中读取文件的时候,大部分的CPU时间用于等待磁盘去读取数据。在这段时间里,CPU非常的空闲。其深层次的原因是对于IO操作,往往是通过硬件直接存取器(DMA)来执行的,也就是说,CPU只需要将发送一个指令给DMA去执行对应的IO操作即可,指令发送是一瞬间的事,发送完成CPU就可以干其他的事了,我们说的IO操作需要执行5秒事实上是DMA执行这个操作需要5秒的时间,而不是CPU。

因此CPU现在很空闲,它可以做一些别的事情。通过改变操作的顺序,就能够更好的使用CPU资源。看下面的顺序:

5秒读取文件A
5秒读取文件B + 2秒处理文件A
2秒处理文件B
---------------------
总共需要12秒

CPU等待第一个文件被读取完。然后开始读取第二个文件。当第二文件在被读取的时候,CPU会去处理第一个文件。这可以以下代码来演示:

public class ResourceThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        final long start=System.currentTimeMillis();
        System.out.println("----------程序开始运行---------");
        System.out.println("读取A文件开始...");
        Thread.currentThread().sleep(5000);
        System.out.println("读取A文件结束,耗时:"+(System.currentTimeMillis()-start)/1000+"秒...开始处理A文件,同时开始读取B文件..");
        Thread t1=new Thread(){
            @Override
            public void run() {
                try {
                    System.out.println("读取B文件开始...");
                    Thread.currentThread().sleep(5000);
                    System.out.println("读取B文件结束,耗时:"+(System.currentTimeMillis()-start)/1000+"秒...开始处理B文件");
                    Thread.currentThread().sleep(2000);
                    System.out.println("B文件处理完成...");
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        Thread.currentThread().sleep(2000);
        System.out.println("A文件处理完成...");
        t1.join();
        System.out.println("总耗时:"+(System.currentTimeMillis()-start)/1000+"秒");
    }
}

在改进后的代码中,我们将B文件的操作放在了另外一个线程中执行,所以效率可以得到提升。这是因为我们在A文件读取完成之后,同时开始了A文件的处理和B文件的处理工作。程序的运行结果如下:

----------程序开始运行---------
读取A文件开始...
读取A文件结束,耗时:5秒...开始处理A文件,同时开始读取B文件..
读取B文件开始...
A文件处理完成...
读取B文件结束,耗时:10秒...开始处理B文件
B文件处理完成...
总耗时:12秒

当然上述代码还有继续改进的空间。因为现在我们是在A文件读取完成之后在读取B文件,我们完全可以同时开启两个线程,两个线程一个用于读取和处理A文件,一个用户读取和处理B文件。

记住,在等待磁盘读取文件的时候,CPU大部分时间是空闲的。

总的说来,CPU能够在等待IO的时候做一些其他的事情。这个不一定就是磁盘IO。它也可以是网络的IO,或者用户输入。通常情况下,网络和磁盘的IO比CPU和内存的IO慢的多。

程序设计更简单

在单线程应用程序中,如果你想编写程序手动处理上面所提到的读取和处理的顺序,你必须记录每个文件读取和处理的状态。相反,你可以启动两个线程,每个线程 处理一个文件的读取和操作。线程会在等待磁盘读取文件的过程中被阻塞。在等待的时候,其他的线程能够使用CPU去处理已经读取完的文件。其结果就是,磁盘 总是在繁忙地读取不同的文件到内存中。这会带来磁盘和CPU利用率的提升。而且每个线程只需要记录一个文件,因此这种方式也很容易编程实现。

程序响应更快

将一个单线程应用程序变成多线程应用程序的另一个常见的目的是实现一个响应更快的应用程序。设想一个服务器应用,它在某一个端口监听进来的请求。当一个请求到来时,它去处理这个请求,然后再返回去监听。

服务器的流程如下所述:

while(server is active){
    listen for request
    process request
}

如果一个请求需要占用大量的时间来处理,在这段时间内新的客户端就无法发送请求给服务端。只有服务器在监听的时候,请求才能被接收。另一种设计是, 监听线程把请求传递给工作者线程(worker thread),然后立刻返回去监听。而工作者线程则能够处理这个请求并发送一个回复给客户端。这种设计如下所述:

while(server is active){
    listen for request
    hand request to worker thread
}

这种方式,服务端线程迅速地返回去监听。因此,更多的客户端能够发送请求给服务端。这个服务也变得响应更快.

桌面应用也是同样如此。如果你点击一个按钮开始运行一个耗时的任务,这个线程既要执行任务又要更新窗口和按钮,那么在任务执行的过程中,这个应用程 序看起来好像没有反应一样。相反,任务可以传递给工作者线程(work thread)。当工作者线程在繁忙地处理任务的时候,窗口线程可以自由地响应其他用户的请求。当工作者线程完成任务的时候,它发送信号给窗口线程。窗口 线程便可以更新应用程序窗口,并显示任务的结果。对用户而言,这种具有工作者线程设计的程序显得响应速度更快。