并发编程入门(四):显式锁和线程池

37 阅读6分钟

目录

前言

Java中的Lock(显式锁)

1. Lock接口

2. 重入锁ReentrantLock

2.1. 快速上手

 2.2. 确保已获取锁的释放

2.3. 避免锁交叉使用导致死锁

2.4. 可重入性

3. 读写锁ReadWriteLock和ReentrantReadWriteLock

3.1. 快速上手

4. Condition的使用

4.1. 快速上手

 5. 锁的优化

6.小结

线程池

1. 简单入门

1.1. 介绍

1.2. 线程池执行机制

 1.3. 线程池快速上手

1.4. 合理配置线程池:

2.ExecutorService

2.1.Executor和ExecutorService

2.2. ScheduleThreadPoolExecutor

2.3. 关闭ExecutorService

 2.4. Executors

2.5. Future与线程池

2.7. CompletationService

3. 小结


前言

本文是《Java并发视频入门》视频课程的笔记总结,旨在帮助更多同学入门并发编程。

本系列共五篇博客,本篇博客着重聊显式锁和线程池。

侵权删。原视频地址:【多线程】Java并发编程入门_哔哩哔哩_bilibili本课程是Java并发编程入门版课程,适合基础薄弱的同学进行学习。如需完整版请移步腾讯课堂进行购买:https://ke.qq.com/course/3486171?tuin=5e1f405a更多优质课程请上腾讯课堂搜索“雷俊华老师”:https://ke.qq.com/course/3486171?tuin=5e1f405ahttps://www.bilibili.com/video/BV1a5411u7o7?p=1

博客汇总地址:《并发编程入门》总结篇_勤修的博客-CSDN博客本文是《Java并发视频入门》视频课程的笔记总结,旨在帮助更多同学入门并发编程。本系列共五篇博客,此文为五篇博客的汇总篇。https://kevinhe.blog.csdn.net/article/details/125143179

Java中的Lock(显式锁)

Sychronized并非显式锁,什么时候加/解锁并不清楚。一般来说:一个锁能够防止多个线程同时访问共享资源(读写锁允许多个线程访问共享资源)。

1. Lock接口

提供了Sychronized关键字类似的同步功能,使用时显式的获取和释放锁,虽然缺少Sychronized的便捷性,但拥有更大的灵活性。

Lock的不同之处:

  1. 允许以非阻塞方式获取锁eg:tryLock();Sychronized只能以阻塞的方式获取,获取不到,会一直等待。
  2. 可中断地获取锁,eg:lockInterrptbly()。
  3. 超时获取锁,Lock接口提供了超时时间。

2. 重入锁ReentrantLock

何为重入?当一个线程获取到锁后,同一个线程再次获取到锁时,也是可以进入的。Sychronized是可重入锁。Sychronized与ReentrantLock功能相同,但不同之处在于:Sychronized是从JVM层面进行锁的获取和释放,使用monitor enter和monitor exit命令。Lock是API显式获取和释放。

2.1. 快速上手

public class Test1 {
    private static Lock lock = new ReentrantLock();
    private static int value = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();//加锁
                    try {
                        value++;
                        if (value == 3) {
                            //第三个线程抛出异常,并未执行到unLock解锁,其他线程得等它解锁。
                            throw new RuntimeException("执行失败");
                        }
                        System.out.println(Thread.currentThread().getId() + ",value is:" + value);
                    } catch (Exception e) {
                        e.printStackTrace();
                        //无论前面执行什么,都会执行finally方法,哪怕try里面有return
                    } finally {
                        //保证解锁代码在第一行,保证一定能解锁成功
                        lock.unlock();//解锁
                    }

                }
            }).start();
        }
    }
}
结果输出:
14,value is:1
15,value is:2
17,value is:4
18,value is:5
19,value is:6
20,value is:7
21,value is:8
22,value is:9
23,value is:10
java.lang.RuntimeException: 执行失败
        at com.company.unit6.Test1$1.run(Test1.java:20)
        at java.base/java.lang.Thread.run(Thread.java:832)

 2.2. 确保已获取锁的释放

使用Sychronized不需要关注锁释放问题,JVM会替我们释放,使用Lock接口,必须要关注锁释放问题。

正确做法是:获取锁之后紧跟着try代码块,并在finally第一行进行解锁。确保不管程序执行是否异常,都能解锁成功。

2.3. 避免锁交叉使用导致死锁

Sychronized可能会导致死锁,同样的,Lock可能也会导致死锁。

2.4. 可重入性

同一个线程多次获取同一把锁,能够获取成功。Lock满足这个特性。

public class Test2 {
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
       method1();
    }

    private static void method1() {
        lock.lock();
        try {
            System.out.println("方法1");
            method2();
        } finally {
            lock.unlock();
        }

    }

    private static void method2() {
        lock.lock();
        try {
            System.out.println("方法2");
            method3();
        } finally {
            lock.unlock();
        }
    }

    private static void method3() {
        lock.lock();
        try {
            System.out.println("方法3");
        } finally {
            lock.unlock();
        }
    }
}
结果输出:
方法1
方法2
方法3

3. 读写锁ReadWriteLock和ReentrantReadWriteLock

共享资源一般是读写两个操作,当多个线程对共享资源执行读操作时,不会引起共享资源数据不一致的问题。读写锁允许特定时刻多线程并发读取共享资源,提高了吞吐量。

3.1. 快速上手

public class Test3 {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    private final LinkedList<String> list = new LinkedList<>();

    /**
     * 写入时候加写锁
     * @param element
     */
    public void add(String element) {
        writeLock.lock();
        try {
            System.out.println("正在做写入"+ element + "的操作");
            list.add(element);
        } finally {
            writeLock.unlock();
        }
    }


    /**
     * 读取时候加读锁
     * @param index
     */
    public void get (int index) {
        readLock.lock();
        try {
            System.out.println("正在读下标"+ index + "的操作");
            String s = list.get(index);
            System.out.println(s);
        } finally {
            readLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test3 test3 = new Test3();
        for (int i = 0; i < 1; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 30; j++) {
                        test3.add(j + "");
                        try {
                            TimeUnit.SECONDS.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(2);

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 3; j++) {
                        test3.get(j);
                        try {
                            TimeUnit.SECONDS.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    }
}
结果输出:
正在做写入0的操作
正在做写入1的操作
正在读下标0的操作
0
正在读下标0的操作
0
正在读下标0的操作
0
正在做写入2的操作
正在读下标1的操作
1
正在读下标1的操作
1
正在读下标1的操作
1
正在做写入3的操作
正在读下标2的操作
正在读下标2的操作
2
正在读下标2的操作
2
2
正在做写入4的操作
正在做写入5的操作
正在做写入6的操作
正在做写入7的操作
...

 上述代码,写操作会互斥处理,每次间隔在1s左右;读操作虽然加了锁,但并不互斥。

读写锁适用于写线程较少,读线程很多,会有相当可观的性能。如果写线程较多,可能会出现性能问题。当读写线程相同时,更推荐使用ReentrantLock。

4. Condition的使用

线程间通信方式,我们提到了notify、wait和natifyAll,使用条件是持有同一把锁,而且要在Sychronized关键字内。Lock时的线程通信我们使用Condition替代notify等。

4.1. 快速上手

交替输出。

public class Test4 {
    private static int shareData = 0;
    //定义布尔变量当前共享数据是否已经被使用
    private static boolean dataUsed = false;
    private static final Lock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    private static void write() {
        lock.lock();
        try {
            //如果当前数据未使用,当前线程进入等待,并释放lock.
            while (!dataUsed) {
                condition.await();
            }
            //如果数据被使用了,修改数据,修改之后标识未使用
            TimeUnit.SECONDS.sleep(1);
            shareData++;
            dataUsed = false;
            System.out.println("共享数据值被修改了" + shareData);
            condition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
          lock.unlock();
        }
    }

    private static void read() {
       lock.lock();
       try {
           //如果当前数据已经被使用,当前线程进入等待,并释放lock
           while (dataUsed) {
               condition.await();
           }
           TimeUnit.SECONDS.sleep(1);
           System.out.println("线程读取到共享数据:" + shareData);
           dataUsed = true;
           condition.signal();
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
          lock.unlock();
       }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
              while (true) {
                  write();
              }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    read();
                }
            }
        }).start();
    }
}
结果输出:
线程读取到共享数据:0
共享数据值被修改了:1
线程读取到共享数据:1
共享数据值被修改了:2
线程读取到共享数据:2
共享数据值被修改了:3
线程读取到共享数据:3
共享数据值被修改了:4
线程读取到共享数据:4

 5. 锁的优化

1.减少锁的持有时间:

只在线程安全要求的情况下加锁,尽量减少同步代码块对锁的持有时间;

2.减少锁粒度:

将单个耗时较多的锁操作拆分为多个耗时较少锁操作来增加锁的并行度。eg:ConCurrentHashMap的分段锁。

3.锁分离:

根据不同场景将锁的功能进行分离,以应对不同的变化,比如读写锁ReadWriteLock。将锁分为读锁和写锁。读读不互斥、读写互斥、写写互斥。

4.锁粗化:

与“减少锁粒度”相反,锁分的太细,会导致频繁获取和释放锁。将关联性较强的锁集中进行处理。

5.锁消除:

不需要锁的地方没必要加锁。

6.小结

本小节我们介绍了Lock与Sychronized的区别、可重入锁ReentrantLock的使用(快速上手、确保释放、防止交叉使用导致死锁、可重入性)、读写锁ReentrantReadWriteLock和线程间Lock通信模式Condition的使用等。

线程池

1. 简单入门

1.1. 介绍

什么是线程池?对象的创建和销毁是耗时的,需要资源,JVM会跟踪每一个对象,以便对象销毁后进行垃圾回收。提高效率的一个方式是减少对象的创建和销毁次数。线程池顾名思义,创建若干个可执行线程放在池子(容器)中,需要的时候从池子中取而非自行创建,使用完成后并非销毁而是放到池子中。减少创建、销毁对象带来的开销。

线程池的优势?降低资源消耗(重复利用已创建线程降低消耗);提高响应速度(无需等待线程创建就能时间);提高线程可管理性(线程无法无限制创建,统一进行调优监控)。

1.2. 线程池执行机制

主要参数有6个:

  1. corePoolSize(核心线程数):向线程池提交任务,若线程池已创建线程小于corePoolSize,创建一个新线程来执行任务,直到已创建线程大于等于corePoolSize。(除了按需构造外,也可提前预创建线程池的基本线程);
  2. workQueue(任务队列):传输和保存执行任务的阻塞队列;eg:4个核心线程,第5个任务进来后会进入任务队列,直到核心线程释放后进入。
  3. maximumPoolSize(线程最大大小):线程池所允许的最大线程个数,eg:任务队列满时,会再次创建线程,创建至最大大小为止。
  4. keepAliveTime(线程存活时间):线程池中线程数大于核心线程数,线程空闲时间超过线程存活时间,该线程就会被销毁,直到线程池中线程数小于等于核心线程数。
  5. handler(线程饱和策略):当线程池和任务队列都满时,再加入线程会执行此策略。eg:丢弃、报错或自定义。
  6. threadFactory(线程工厂):用于创建新线程,new Thread方式。线程名具有统一风格:pool-m-thread-n(m是线程池编号,n为线程池的线程编号)

处理流程:

  1. 判断核心线程池是否已满,否,创建线程;
  2. 判断队列是否已满,否,将任务存储在队列中;
  3. 判断线程池是否已满,否,创建线程执行任务,是,按照策略处理。

 1.3. 线程池快速上手

public class Test1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5, 10, 1L,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());
        //submit和executor的区别:submit有返回值,executor没有。
        List<Future<String>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Runnable runnable = new MyRunnable(i + "");
            executor.execute(runnable);
            final int temp = i;
            Future<String> submit = executor.submit(() -> {
                return temp + "";
            });
            list.add(submit);
        }
        for (Future<String> future: list) {
            System.out.println(future.get());
        }
    }
}


class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String command) {
        this.command = command;
    }


    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "启动,当前时间:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束,当前时间:" + new Date());
    }

    @Override
    public String toString() {
        return command;
    }
}
结果输出:
0
1
2
3
pool-1-thread-4启动,当前时间:Sun Jun 05 23:14:43 CST 2022
pool-1-thread-2启动,当前时间:Sun Jun 05 23:14:43 CST 2022
pool-1-thread-5启动,当前时间:Sun Jun 05 23:14:43 CST 2022
pool-1-thread-3启动,当前时间:Sun Jun 05 23:14:43 CST 2022
pool-1-thread-1启动,当前时间:Sun Jun 05 23:14:43 CST 2022
pool-1-thread-3结束,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-2结束,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-4结束,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-2启动,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-5结束,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-4启动,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-1结束,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-5启动,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-3启动,当前时间:Sun Jun 05 23:14:48 CST 2022
pool-1-thread-1启动,当前时间:Sun Jun 05 23:14:48 CST 2022
4
5
6
7
8
pool-1-thread-2结束,当前时间:Sun Jun 05 23:14:53 CST 2022
pool-1-thread-3结束,当前时间:Sun Jun 05 23:14:53 CST 2022
pool-1-thread-5结束,当前时间:Sun Jun 05 23:14:53 CST 2022
pool-1-thread-4结束,当前时间:Sun Jun 05 23:14:53 CST 2022
pool-1-thread-1结束,当前时间:Sun Jun 05 23:14:53 CST 2022
9

1.4. 合理配置线程池:

首先分析任务:

  1. 任务性质:CPU密集型(大量运算)、IO密集型(查询数据库)和混合型;
  2. 任务优先级:高、中、低;
  3. 任务执行时间:长、中、短;
  4. 任务依赖性:是否依赖其他资源,比如数据库连接。

针对性质不同,配置多个线程池。CPU密集型配置尽可能小的线程,配置CPU+1个线程的线程池;IO密集型并不是一直在执行任务,应当配置足够多的线程,配置2*CPU线程的线程池。

//获取CPU个数
System.out.println(Runtime.getRuntime().availableProcessors());
结果输出:
12

 针对优先级,可以使用PriorityQueue来处理;

针对执行时间,交给不同规模的线程池进行处理,或者可以使用优先队列,让执行时间短的线程先执行。

针对任务依赖性,等待时间越长,CPU空闲时间越长,线程数设置的越大越好。

2.ExecutorService

2.1.Executor和ExecutorService

线程池除了ThreadPoolExecutor类,还包括Executor和ExecutorService。Executor父类。

2.2. ScheduleThreadPoolExecutor

任务可以被定时执行。

public class Test4 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
        //1.schedule:该方法是只执行一次的方法,任务ICallable会在单位时间后被执行,并立即返回ScheduledFuture,在稍后的任务中可以通过Future来获取异步任务的执行结果。
        ScheduledFuture<String> future = executor.schedule(()-> {
            System.out.println("开始执行任务");
            return "hello";
        }, 10, TimeUnit.SECONDS);
        //2.scheduleAtFixedRate:任务会根据固定的速率,在initialDelay时间之后不断地被执行。
        ScheduledThreadPoolExecutor executor2 = new ScheduledThreadPoolExecutor(2);
        executor2.scheduleAtFixedRate(()-> {
            System.out.println(System.currentTimeMillis());
        }, 5, 1, TimeUnit.SECONDS);
        //3.scheduleWithFixedDelay:前面方法执行完后,再执行1s间隔操作。
        ScheduledThreadPoolExecutor executor3 = new ScheduledThreadPoolExecutor(2);
        executor3.scheduleWithFixedDelay(()-> {
            long start = System.currentTimeMillis();
            System.out.println("当前时间" + start);
            try {
                TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行耗时" + (System.currentTimeMillis() - start));
        }, 5, 1, TimeUnit.SECONDS);
    }
}
结果输出:
case1:
开始执行任务
case2:
1654442721958
1654442722958
1654442723954
1654442724958
1654442725958
case3:
当前时间1654442606656
执行耗时559
当前时间1654442608221
执行耗时446
当前时间1654442609672
执行耗时958
当前时间1654442611635
执行耗时124
当前时间1654442612764
执行耗时738
当前时间1654442614504

2.3. 关闭ExecutorService

有序关闭:新任务提交将会被拒绝,工作线程执行的任务和线程池任务队列中已经提交的任务将会被执行,当所有任务执行完毕后才会关闭线程池。

立即关闭:立马停止。

public class Test7 {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, 4, 1L,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), new ThreadPoolExecutor.CallerRunsPolicy());
        for (int i = 0; i < 10; i++) {
            executor.execute(() ->  {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程执行完毕:" + Thread.currentThread());
            });
        }
//        executor.shutdown();
        List<Runnable> list = executor.shutdownNow();
        executor.execute(() ->  {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程执行完毕2:" + Thread.currentThread());
        });
    }
}
结果输出:
case1:shutDown
线程执行完毕:Thread[pool-1-thread-1,5,main]
线程执行完毕:Thread[pool-1-thread-2,5,main]
线程执行完毕:Thread[pool-1-thread-1,5,main]
线程执行完毕:Thread[pool-1-thread-2,5,main]
线程执行完毕:Thread[pool-1-thread-1,5,main]
线程执行完毕:Thread[pool-1-thread-2,5,main]
线程执行完毕:Thread[pool-1-thread-2,5,main]
线程执行完毕:Thread[pool-1-thread-1,5,main]
线程执行完毕:Thread[pool-1-thread-2,5,main]
线程执行完毕:Thread[pool-1-thread-1,5,main]
case2: shutdownNow
java.lang.InterruptedException: sleep interrupted
        at java.base/java.lang.Thread.sleep(Native Method)
        at java.base/java.lang.Thread.sleep(Thread.java:337)
        at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
        at com.company.unit8.Test7.lambda$main$0(Test7.java:16)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
        at java.base/java.lang.Thread.run(Thread.java:832)
java.lang.InterruptedException: sleep interrupted
        at java.base/java.lang.Thread.sleep(Native Method)
        at java.base/java.lang.Thread.sleep(Thread.java:337)
        at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
        at com.company.unit8.Test7.lambda$main$0(Test7.java:16)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
        at java.base/java.lang.Thread.run(Thread.java:832)
线程执行完毕:Thread[pool-1-thread-2,5,main]
线程执行完毕:Thread[pool-1-thread-1,5,main]

 2.4. Executors

ThreadPoolExecutor比较复杂,Executors工具类为我们创建不同的ExecutorService。Executors创建的线程池各有弊端,建议手动创建。

FixThreadPool

//使用
Executors.newFixedThreadPool(10);
//源码
new ThreadPoolExecutor(nThreads, nThreads,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>())

可重用的固定线程的线程池。特点为:核心线程数=最大线程数;LinkedBlockingQueue无边界阻塞队列,提交至线程池的任务都会被执行。

不推荐使用。原因为:LinkedBlockingQueue是无界队列,会带来如下影响:

  1. 线程池线程数不会超过corePoolSize;
  2. maximumPoolSize和keepAliveTime是无效值;
  3. 运行中的FixedThreadPool不会拒绝任务,任务较多时可能会导致OOM(内存溢出)。

SingleThreadExecutor

//使用
Executors.newSingleThreadExecutor();
//源码
new ThreadPoolExecutor(1, 1,
                       0L, TimeUnit.MILLISECONDS,
                       new LinkedBlockingQueue<Runnable>()));

只有一个核心线程的线程池。

不推荐使用,原因为:与FixThreadPool相同,可能会导致OOM。

CachedThreadPool

//使用
Executors.newCachedThreadPool();
//源码
new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                              60L, TimeUnit.SECONDS,
                              new SynchronousQueue<Runnable>());

根据需要创建线程的线程池。适用于执行量大、耗时短、异步任务程序。

不推荐使用。原因为:核心线程数为Integer.MAX_VALUE,创建大量线程导致OOM。

ScheduleThreadPool

创建一个制定线程数量的定时任务。

WorkStealingPool

Fork/Join框架衍生出来的线程池。

2.5. Future与线程池

Future代表了异步任务在未来的执行结果,提供了get方法,调用此方法后,线程会立即阻塞,直到该任务执行完毕返回为止。

invokeAny:批量处理,阻塞方法,等待直到有一个任务执行完成,只关心第一个完成的任务和结果。eg:获取某城市天气情况,需要调用不同的服务提供商接口,最快返回的数据将显示天气情况。

invokeAll:批量处理,关注所有任务的返回结果,同样是阻塞方法。

public class Test9 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5, 10, 1L,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());
        List<Callable<Integer>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(()-> {
                int random = ThreadLocalRandom.current().nextInt(30);
                TimeUnit.SECONDS.sleep(random);
                System.out.println("任务" + Thread.currentThread() + "休眠了" + random + "s");
                return random;
            });
        }
//        Integer integer = executor.invokeAny(list);
//        System.out.println(integer);
        List<Future<Integer>> futures = executor.invokeAll(list);
        futures.forEach(e -> {
            try {
                System.out.println(e.get());
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            } catch (ExecutionException executionException) {
                executionException.printStackTrace();
            }
        });
    }
}
结果输出:
case1:invokeAny
任务Thread[pool-1-thread-5,5,main]休眠了7s
7
case2:invokeAll
任务Thread[pool-1-thread-4,5,main]休眠了2s
任务Thread[pool-1-thread-3,5,main]休眠了9s
任务Thread[pool-1-thread-2,5,main]休眠了11s
任务Thread[pool-1-thread-4,5,main]休眠了13s
任务Thread[pool-1-thread-4,5,main]休眠了6s
任务Thread[pool-1-thread-1,5,main]休眠了23s
任务Thread[pool-1-thread-5,5,main]休眠了27s
任务Thread[pool-1-thread-3,5,main]休眠了21s
任务Thread[pool-1-thread-4,5,main]休眠了18s
任务Thread[pool-1-thread-2,5,main]休眠了29s
23
11
9
2
27
13
21
29
6
18

2.7. CompletationService

采用了异步任务提交和计算结果Future解耦的一种方式。我们进行任务的提交,通过操作队列的方式来消费Future。

public class Test10 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newCachedThreadPool();
        CompletionService<Integer> completionService = new ExecutorCompletionService<>(executor);
        List<Callable<Integer>> list = Arrays.asList(
                () -> {
                    TimeUnit.SECONDS.sleep(30);
                    return 30;
                }, () -> {
                    TimeUnit.SECONDS.sleep(10);
                    return 10;
                },() -> {
                    TimeUnit.SECONDS.sleep(20);
                    return 20;
                }
        );
        //常规线程池:关注执行结果必须用Future,输出为:30-10-20。
        //问题:批量提交,按照任务提交顺序get。第一个执行的是30s的任务。
//        List<Future<Integer>> futures = executor.invokeAll(list);
//        for (Future<Integer> future: futures) {
//            System.out.println(future.get());
//        }
        //CompletionService。输出为:10-20-30。
        //问题:异步任务提交和计算结果Future解耦。任务执行完后会放在一个队列中,谁先执行完谁就在第一个。
        for (Callable<Integer> callable: list) {
            completionService.submit(callable);
        }
        ///业务逻辑
        for (int i = 0; i < list.size(); i++) {
            System.out.println(completionService.take().get());
        }
    }
}
结果输出:
10
20
30

3. 小结

本小节我们介绍了线程池是什么?怎么运行的?如何快速上手?如何配置相关参数?

随后我们介绍了如何开启和关闭ExecutorService;分析了继承自Executors的几种线程池的优缺点。

最后我们介绍了Future与线程池、CompletationService。