在工程项目中可能会有这么一个场景,客户端处理层需要从服务端(CDN/图片服务器)获取n张图片(参考微博一个人最多有9张图片),那么问题来了,如何在一定的时间范围内尽可能多的获取到图片。当然,最为简单粗暴的方法就是通过串行的方式来获取,但是如果第一个请求hang住并超时,那么其他图片都无法获取,这显然不是一个好的设计方式。这个问题设计到两点,第一点是"尽可能多",相比于串行,解决方案就是多线程并行;第二点是"一定时间范围内",也就是超时时间,因此这个问题就可以抽象为多线程在超时范围内获取数据的方法,下面探讨几张方法。


一、thread.join(long millis) (不满足需求)

       在JavaDoc中join(time)功能为在线程退出之前将会最多等待time毫秒。事实上,如果超时时间过了,那么主线程将会继续停止阻塞(可见join方法是阻塞的)并继续执行,这么做有一个弊端可能会造成所谓的"线程泄露"。此外,我们通常会调用interrupt来中断线程,当然这么做也是不提倡的。

TimeoutDemo.java

public class TimeoutDemo {

    public static void main(String[] args) {

        try {

            int threadCount = 3;

            int defaultTimes = 10;

            Thread[] ts = new Thread[threadCount];

            for (int i=0; i < threadCount; i++) {
                ts[i] = new Thread(new TimeoutRunnable(defaultTimes));
            }

            for (int i=0; i < threadCount; i++) {
                ts[i].start();
            }

            for (int i=0; i < threadCount; i++) {
                ts[i].join(1000);
                System.out.println("i = " + i + " has joined...");
            }

            for (int i=0; i < threadCount; i++) {
                ts[i].interrupt();
            }

        } catch (Exception e) {
            System.out.println("thread interrupted when waiting join.");
        } finally {
            System.out.println("all threads finished...");
        }
    }
}

class TimeoutRunnable implements Runnable{

    private int times;

    public TimeoutRunnable(int times) {
        this.times = times;
    }

    @Override
    public void run() {
        for (int i = 0; i < times; i++) {
            System.out.println("thread id: " + Thread.currentThread().getId() + " runs for " + (i+1) + "th time.");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("thread id: " + Thread.currentThread().getId() + " is interrupted while running and stop right now.");

                return;
            }
        }

        System.out.println("thread id: " + Thread.currentThread().getId() + " finished.");
    }
}

运行结果

thread id: 11 runs for 1th time.
thread id: 13 runs for 1th time.
thread id: 12 runs for 1th time.
i = 0 has joined...
thread id: 12 runs for 2th time.
thread id: 11 runs for 2th time.
thread id: 13 runs for 2th time.
i = 1 has joined...
thread id: 11 runs for 3th time.
thread id: 12 runs for 3th time.
thread id: 13 runs for 3th time.
i = 2 has joined...
thread id: 13 runs for 4th time.
thread id: 11 runs for 4th time.
thread id: 12 runs for 4th time.
thread id: 11 is interrupted while running and stop right now.
thread id: 13 is interrupted while running and stop right now.
all threads finished...
thread id: 12 is interrupted while running and stop right now.

可见,join方法是阻塞的,依次等待线程超时,就本例来讲,等待了3个1000ms,不满足在一定的timeout内完成。


二、基于ExecutorService的shutdown() 和 awaitTermination(long timeout, TimeUnit unit)方法:

        awaitTermination是一个阻塞方法,在ExecutorService执行完shutdown之后进行操作,awaitTermination将会阻塞,直到所有任务完成了执行,或者超时产生,或者线程中断发生为止。如果所有任务完成则返回true;如果在任务完成之前发生了超时则返回false。代码示例,TimeoutDemo1.java

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TimeoutDemo1 {

    public static void main(String[] args) {

        try {

            int threadCount = 10;

            int defaultTimes = 10;

            ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

            for (int i=0; i < threadCount * 2; i++) {
                executorService.submit(new TimeoutRunnable1(defaultTimes));
            }

            executorService.shutdown();

            if(!executorService.awaitTermination(7, TimeUnit.SECONDS)) {
                System.out.println("executors did not terminate in the specified time.");
                List<Runnable> droppedTasks = executorService.shutdownNow();
                System.out.println("executors was abruptly shut down. " + droppedTasks.size() + " tasks will not be executed.");
            }
        } catch (Exception e) {
            System.out.println("exception: " + e.getMessage());
        } finally {
            System.out.println("scheduled executor service closed normally.");
        }
    }
}

class TimeoutRunnable1 implements Runnable{

    private int times;

    public TimeoutRunnable1(int times) {
        this.times = times;
    }

    @Override
    public void run() {
        for (int i = 0; i < times; i++) {
            System.out.println("thread id: " + Thread.currentThread().getId() + " runs for " + (i+1) + "th time.");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("thread id: " + Thread.currentThread().getId() + " is interrupted while running and stop right now.");

                return;
            }
        }
        System.out.println("thread id: " + Thread.currentThread().getId() + " finished.");
    }
}

关于shutdown和shutdonwNow的区别参考前一篇文章:如何优雅的关闭线程池。

三、使用CountDownLatch中的await(long timeout, TimeUnit unit)

    如果在timeout时间范围内,count变成了0,那么await返回true;如果超过了超时时间,count不为0,那么返回false。

看代码TimeoutDemo2.java

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TimeoutDemo2 {

    public static void main(String[] args) {

        try {

            int threadCount = 10;

            int defaultTimes = 10;

            CountDownLatch countDownLatch = new CountDownLatch(threadCount * 2);

            ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

            for (int i=0; i < threadCount * 2; i++) {
                executorService.submit(new TimeoutRunnable2(defaultTimes, countDownLatch));
            }

            if(!countDownLatch.await(19, TimeUnit.SECONDS)) {
                executorService.shutdown();
                System.out.println("executors did not terminate in the specified time.");
                List<Runnable> droppedTasks = executorService.shutdownNow();
                System.out.println("executors was abruptly shut down. " + droppedTasks.size() + " tasks will not be executed.");
            }
        } catch (Exception e) {
            System.out.println("exception: " + e.getMessage());
        } finally {
            System.out.println("scheduled executor service closed normally.");
        }
    }
}

class TimeoutRunnable2 implements Runnable{

    private int times;

    private CountDownLatch countDownLatch;

    public TimeoutRunnable2(int times,  CountDownLatch countDownLatch) {
        this.times = times;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < times; i++) {
                System.out.println("thread id: " + Thread.currentThread().getId() + " runs for " + (i + 1) + "th time.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("thread id: " + Thread.currentThread().getId() + " is interrupted while running and stop right now.");

                    return;
                }
            }

            System.out.println("thread id: " + Thread.currentThread().getId() + " finished.");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            countDownLatch.countDown();
        }
    }
}

四、Future.get(long timeout, TimeUnit unit)(不完全满足需求,不最优)

      future.get在一定的超时时间范围内获取线程执行的返回值,但是get方法是阻塞的,会在一定的超时时间之内阻塞,直到任务完成返回,或者这时间超时抛出TimeoutException,代码如下:

TimeoutDemo3.java:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class TimeoutDemo3 {

    public static void main(String[] args) {

        try {
            int threadCount = 10;

            int defaultTimes = 10;

            ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

            List<Future> futureList = new ArrayList<Future>();

            for (int i=0; i < threadCount; i++) {
                Future future = executorService.submit(new TimeoutRunnable3(defaultTimes));
                futureList.add(future);
            }

            for (Future future: futureList) {
                String str = (String)future.get(15, TimeUnit.SECONDS);

                System.out.println(str);
            }
        } catch (InterruptedException e) {
            System.out.println("future is interrupted...");
        } catch (ExecutionException e) {
            System.out.println("future execution exception...");
        } catch (TimeoutException e) {
            System.out.println("future timeout exception...");
        } finally {
            System.out.println("scheduled executor service closed normally.");
        }
    }
}

class TimeoutRunnable3 implements Callable<String>{

    private int times;

    public TimeoutRunnable3(int times) {
        this.times = times;
    }

    @Override
    public String call() {
        try {
            for (int i = 0; i < times; i++) {
                System.out.println("thread id: " + Thread.currentThread().getId() + " runs for " + (i + 1) + "th time.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("thread id: " + Thread.currentThread().getId() + " is interrupted while running and stop right now.");
                }
            }

            //System.out.println("thread id: " + Thread.currentThread().getId() + " finished.");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        return "thread id: " + Thread.currentThread().getId() + " finished";
    }
}

这么做的问题在于,一旦最前面的线程阻塞了,超时了并抛出异常,那么其他完成的线程都无法拿到结果。

Author:忆之独秀