并发编程——Java线程池的创建

363 阅读8分钟

Java线程池的创建

1、为什么要使用线程池?

创建线程的开销比较大,使用现有的池子内的资源比较节省成本。

举个例子,假设有个玩具厂,生产玩具的时候需要工人参与。临时创建线程就好比有订单来的时候再临时招工,需要花一定的时间和资源,通过线程池创建线程就类比工厂中早就雇佣了一些工人,有了订单就不需要再临时招工,成本较低。

2、如何创建线程池:使用ThreadPoolExecutor

在日常的使用中,ThreadPoolExecutor已经可以满足创建不同种类线程池的需求。

常用的创建方法:摘自Executors.java

ExecutorService newFixedThreadPool(int nThreads);
ExecutorService newWorkStealingPool(int parallelism);
ExecutorService newSingleThreadExecutor();
ExecutorService newCachedThreadPool();
ScheduledExecutorService newSingleThreadScheduledExecutor();
ScheduledExecutorService newScheduledThreadPool(int corePoolSize);

2.1 创建固定线程数量的线程池:newFixedThreadPool()

方法作用

此方法创建一个线程池,该线程池中运行固定数量的线程,这些线程运行在一个共享的无边界队列上。任何时候最多只有nThreads个线程在处理任务。如果在所有的线程都活跃的状态下提交了额外的任务,这些任务就会在一个队列中等待,直到有可用的线程。如果有线程在执行过程中由于执行任务失败而终止,则会有新的线程代替它执行后续的任务。池中的线程将会一直存在,直到被明确地关闭。(翻译自Java Doc

举个例子

玩具厂现在只有5个工人工作,每个工人可以组装1个玩具。假设来了2个玩具需要组装,那么只需要两个工人即可。假设来了10个玩具需要组装,那么就需要5个工人都工作,并且还有另外5个玩具在队列中等待被组装。假设有一个工人在组装玩具的过程中突然被小零件卡住了,那么将安排另一个工人代替他继续完成新任务。工人会一直工作到被通知下班shutdown,或者工厂关闭。

代码示例
public class ExecutorsDemo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            service.submit(() -> System.out.println("Thread ID: " + 
                    Thread.currentThread().getId()));
        }
    }
}
运行结果

可以发现线程的ID一共只有三个:10、11、12。

2.2 工作窃取:newWorkStealingPool()

方法作用

Work Stealing的核心思想就是:自己的活儿要是做完了就去瞅瞅别人有没有多余的活儿,有的话就去帮他。

常用的实现方式是双端队列。窃取任务的线程从双端队列的尾部拿任务,避免竞争。

使用场合是任务量比较大的时候,可以将任务分割为若干子任务。

举个例子

有3堆毛线需要整理成球,每根毛线有两个端点。甲乙丙三个同时接到这三个任务,他们都拿到一堆毛线开始绕。甲先把自己的那堆毛线绕成球了,看到乙还有一半工作没做完,想帮忙,甲的正确方式不是接过乙手里的端点继续绕,而是从乙那堆毛线的另外一个端点开始绕,这样才能加快任务。

代码示例
public class ExecutorsDemo {
    static CountDownLatch countDownLatch = new CountDownLatch(10);
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newWorkStealingPool(3);
        for (int i = 0; i < 10; i++) {
            service.execute(() -> {
                try {
                    System.out.println(Thread.currentThread().getName());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
    } 
}
运行结果

如图所示,可以看出实现方式是使用ForkJoinPool

2.3 创建单个线程的线程池:newSingleThreadExecutor()

方法作用

该方法创建一个工作在无界队列上的单个工作线程。需要注意,如果该工作线程在关闭之前由于执行任务而失败了,接下来的新任务需要一个新的线程代替该失败线程。任务被保证按照顺序执行,也就是FIFO(先进先出),任何时候的活动线程都不超过一个。与*newFixedThreadPool(1)*不同的是,返回的执行程序不能被重新配置成使用其他线程。

举个例子

煎饼摊有一个煎饼师傅在做煎饼,他一次只能做一个煎饼,他的客户只能按顺序排队来买煎饼,先到先得(FIFO)。假设他有一天手痛没法做煎饼了,他儿子(自己人)可以帮忙继续做煎饼。但是这个煎饼摊已经用水泥固定在家门口了无法出借(不能被重新配置)。

代码示例
public class ExecutorsDemo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 6; i++) {
            service.submit(() -> System.out.println("Thread ID: " +
                    Thread.currentThread().getName()));
        }
    }
}
运行结果

如图所示,可以看出只有一个线程在执行任务。

2.4 创建可调整数量的线程池:newCachedThreadPool()

方法作用

该方法创建一个可以根据需要创建新线程的线程池,但是已有的线程可用时会得到重用。这些线程池可以显著地提高执行很多短期异步任务的程序的性能。执行任务将尽可能地使用先前创建过的线程。如果没有可用的线程,才会创建一个新线程并添加到池中。60s内未使用的线程将被终止并从缓存中删除。因此一个时间保持足够长的线程池不会消耗任何资源。

(暂时没想到例子……)

代码示例
public class ExecutorsDemo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 20; i++) {
            service.submit(() -> System.out.println("Thread ID: "
                    + Thread.currentThread().getId()));
        }
    }
}
运行结果

如图所示,可以看出线程数量比较随机。

2.5 创建可定期执行任务单个线程的线程池:newSingleThreadScheduledExecutor()

该方法创建了一个单线程执行器,这个执行器可以让命令在给定的延迟后执行,或者定期执行。如果该单个线程由于执行任务失败而终止,则使用一个新线程执行新任务。任务的执行顺序也是按顺序执行,并且任务数量不超过一个。

2.5.1 定期执行FixedRate

方法作用:

创建并执行一个周期性命令。该操作在给定的初始延迟后开始执行。然后在给定的时间段内执行。也就是说,执行将在initialDelay,initialDelay + period,initialDelay + 2 * period等之后开始。如果任务在执行过程中出现异常,则禁止后续执行。否则任务只能通过执行器的取消或者终止才能停止。如果该任务的执行时间超过了周期,那么后续的执行会延迟,但是不会同时执行。

方法签名:
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,//要执行的任务
                                              long initialDelay,//延迟首次执行的时间
                                              long period,//连续执行的周期
                                              TimeUnit unit);//时间单位
代码示例:
public class ExecutorsDemo {
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleAtFixedRate(() -> {
            try {
                Thread.sleep(1000);
                System.out.println(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 0, 2, TimeUnit.SECONDS);
    }
}
运行结果

执行结果如图所示,可以看出来任务是以一定频率执行的。每个任务之间的时间差是2秒左右。

2.5.2 延迟执行FixedDelay

方法作用:

创建并执行一个周期性命令。该命令在给定的初始延迟后首先启动,然后在一个执行的终止与下一个执行的开始之间设置给定的延迟。如果任务的任何执行遇到异常,则将禁止后续执行。否则,任务将仅通过取消或终止才能终止。

方法签名:
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,//要执行的命令
                                                 long initialDelay,//延迟首次执行的时间
                                                 long delay,//上一次终止与下一次开始之间的延迟
                                                 TimeUnit unit);//时间单位
代码示例:
public class ExecutorsDemo {
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleWithFixedDelay(() -> {
            try {
                Thread.sleep(1000);
                System.out.println(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 0, 2, TimeUnit.SECONDS);
    }
}
运行结果

执行结果如图所示,可以看出两个任务之间的时间间隔不仅包含线程本身执行的1秒Thread.sleep(1000);还包含任务之间的延迟2秒,共3秒左右。

2.5.3 两者的区别

FixedRate是以任务开始执行的时间开始计算。如果线程执行任务的时间小于设置的period时长,则线程就是按照period的速度在执行。如果线程执行任务的时间长大于period时长,则线程就会在上一个任务结束之后立即调用下一个任务。

FixedDelay是以任务结束的时间开始计算,上一个任务结束后经过delay时间后再调度下一个任务。

2.6 创建可定期执行任务的可指定线程数量的线程池:newScheduledThreadPool()

该方法和上面的差不多,区别在于可以指定线程池的线程数量。

代码示例
public class ExecutorsDemo {
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(3);
        service.scheduleAtFixedRate(() -> {
            try {
                Thread.sleep(1000);
                System.out.println(System.currentTimeMillis());
                System.out.println(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 0, 2, TimeUnit.SECONDS);
    }    
}
运行结果

执行结果如下。可以看出间隔时间还是没变的,但是多使用了几个线程。

本文同步发表于我的公众号,欢迎一起讨论。