面试笔记(二)线程池连环炮

1. 实现多线程有几种方式?有什么区别?

实现多线程有3种方式。

###1.1 继承 Thread 类

继承 Thread 类,重新 run() 方法。实现代码如下:

public class ExtendsThread extends Thread{
    @Override
    public void run() {
        System.out.println("run ExtendsThread");
    }
}

使用线程:

public class LearningThread {
    public static void main(String[] args) {
        func1();
    }
    public static void func1(){
        ExtendsThread extendsThread = new ExtendsThread();
        System.out.println("run func1");
        extendsThread.start();
        System.out.println("run func1 end");
    }
}

1.2 实现 Runnable 接口

实现 Runnable 接口,实现 run() 方法,通过 Thread 类来开启线程。代码如下:

public class ImplRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("run ImplRunnable");
    }
}

使用线程:

public class LearningThread {
    public static void main(String[] args) {
        func2();
    }

    public static void func2(){
        ImplRunnable implRunnable = new ImplRunnable();
        System.out.println("run func2");
        Thread thread = new Thread(implRunnable);
        thread.start();
        System.out.println("run func2 end");
    }
}

1.3 实现 Callable 接口

实现 Callable 接口,实现 call() 方法。代码如下:

public class ImplCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 2020;
    }
}
使用线程:

public class LearningThread {
    public static void main(String[] args) {
        func3();
    }

    public static void func3() throws ExecutionException, InterruptedException {
        Callable<Integer> integerCallable = new ImplCallable();
        System.out.println("run func3");
        FutureTask<Integer> futureTask = new FutureTask<>(integerCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println("run func3 end");
        System.out.println("futureTask.get = " + futureTask.get());
    }
}

这三种方式都可以实现多线程,第一种由于 Java 的单继承,不建议使用。至于实现 Runnable 接口,与实现 Callable 接口,的区别是,实现 Callable 接口后通过 futureTask.get() 方法可以获取线程内的执行结果。而 Runnable 是没有返回值的。

2. 为什么要用线程池?

线程池主要是为了减少每次创建线程时的资源消耗,重复利用创建好的线程,提高资源利用率。使用线程池由于减少了线程创建的过程,在每次接到请求时可以及时响应,提高响应速度。通过线程池统一管理线程,方便线程的分配,调优和监控。

3. 创建线程池的时候有哪些参数?

在 Java 源码中,创建线程池的构造方法最多有 7 个参数。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize: 核心线程数,线程池的工作线程数量。
  • maximumPoolSize: 最大线程数,线程池中可以存活的最多的线程数量。当队列满了之后,会启用非核心线程,此时的线程池的大小变为最大线程数。
  • keepAliveTime: 非核心线程如果没有任务的话,可以存活的时间。
  • unit: 非核心线程存活时间的单位。
  • workQueue: 工作队列。当线程池中的线程达到 corePoolSize ,如果再来任务,就会放到工作队列里。
  • threadFactory: 线程工厂,线程池创建线程时使用的工厂。
  • handler:拒绝策略。如果线程池的线程达到了 maximumPoolSize ,如果再来任务,则执行拒绝策略。

4. 线程池是如何工作的?

当有任务提交到线程池时,首先启动核心线程。随着任务的增加,当核心线程用完之后,再次提交的线程将会进入工作队列。当工作队列满了之后,如果再次提交到线程池任务,将会判断,核心线程数是否小于最大线程数,如果小于,将会启用非核心线程,当工作的线程达到最大线程数后,如果还继续提交任务到线程池,则会执行拒绝策略来拒绝任务。

5. 常见的拒绝策略有哪些?

JDK 自带了 4 种拒绝策略。

5.1 AbortPolicy

直接丢弃任务,抛出 RejectedExecutionException

public class ThreadPoolRunnable implements Runnable {
    private int number = 0;

    public ThreadPoolRunnable(int number) {
        this.number = number;
    }

    @Override
    public void run() {
        System.out.println("run "+Thread.currentThread().getName()+", number = " + number);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使用线程池:

    public static void func4(){
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,10,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 7; i++) {
            Runnable runnable = new ThreadPoolRunnable(i);
            executor.execute(runnable);
        }
        executor.shutdown();
    }

执行结果:

run pool-1-thread-1, number = 0
run pool-1-thread-3, number = 3
run pool-1-thread-2, number = 1
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.learning.thread.ThreadPoolRunnable@1d44bcfa rejected from java.util.concurrent.ThreadPoolExecutor@266474c2[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at com.learning.thread.LearningThread.func4(LearningThread.java:45)
    at com.learning.thread.LearningThread.main(LearningThread.java:11)
run pool-1-thread-1, number = 2

由于最大线程数是 3 ,队列大小是 1,所以线程池最多可以同时存在 4 个线程,当提交第 5 任务时,主线程抛出异常。

5.2 CallerRunsPolicy

调用启用线程池的线程进行处理,不过会阻塞主线程。

    public static void func4(){
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,10,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 7; i++) {
            Runnable runnable = new ThreadPoolRunnable(i);
            executor.execute(runnable);
        }
        executor.shutdown();
    }

执行结果:

run pool-1-thread-1, number = 0
run main, number = 4
run pool-1-thread-3, number = 3
run pool-1-thread-2, number = 1
run main, number = 5
run pool-1-thread-3, number = 2
run pool-1-thread-3, number = 6

可以看到 第 5 个任务,即 number = 4,是在主线程中执行的,由于主线程被阻塞,导致第 6 个任务不能立即提交,当主线程执行结束后再提交时,线程池里已经有可用的线程了,所以第 6 个任务是线程池执行的。

5.3 DiscardPolicy

直接拒绝任务,不抛出任何异常

    public static void func4(){
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,10,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());

        for (int i = 0; i < 7; i++) {
            Runnable runnable = new ThreadPoolRunnable(i);
            executor.execute(runnable);
        }
        executor.shutdown();
    }

执行结果:

run pool-1-thread-2, number = 1
run pool-1-thread-3, number = 3
run pool-1-thread-1, number = 0
run pool-1-thread-2, number = 2

5.4 DiscardOldestPolicy

抛弃队列中最先加入的任务,然后将当前任务提交到线程池。

    public static void func4(){
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,3,10,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardOldestPolicy());

        for (int i = 0; i < 7; i++) {
            Runnable runnable = new ThreadPoolRunnable(i);
            executor.execute(runnable);
        }
        executor.shutdown();
    }
执行结果:

run pool-1-thread-1, number = 0
run pool-1-thread-3, number = 3
run pool-1-thread-2, number = 1
run pool-1-thread-1, number = 6

number 0、1,使用核心线程执行。

number 2,放入队列。

number 3,启用非核心线程执行。

number 4,抛弃 number 2,将 number 4 放入队列

number 5,抛弃 number 4,将 number 5 放入队列

number 6,抛弃 number 5,将 number 6 放入队列

执行 number 6

5.5 自定义拒绝策略

当然,如果上面的拒绝策略都不满足的话,我们也可以定义拒绝策略。

public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(executor.toString());
    }
}

执行结果:

run pool-1-thread-2, number = 1
run pool-1-thread-3, number = 3
run pool-1-thread-1, number = 0
java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
run pool-1-thread-2, number = 2

6. 常见的线程池有哪些?各自有什么特点?

JDK自带了几个常见的线程池。

6.1 FixedThreadPool

FixedThreadPool 被称为可重用固定线程数的线程池。创建的源码如下:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

可以看到 FixedThreadPool 的的核心线程数和最大线程数都是传入的参数。使用的队列是 LinkedBlockingQueue 。
由于 LinkedBlockingQueue 是一个无界队列(队列的容量为 Intger.MAX_VALUE),所以运行中的 FixedThreadPool 不会拒绝任务,所以当任务过多的时候可能会造成 OOM 。

6.2 SingleThreadExecutor

SingleThreadExecutor 是只有一个线程的线程池。创建源码:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

SingleThreadExecutor 的核心线程数和最大线程数都为 1,所以这个线程池只有一个线程。使用的队列也是 LinkedBlockingQueue ,所以当任务过多时也会存在 OOM 的问题。

6.3 CachedThreadPool

CachedThreadPool 无固定大小的线程池,随着任务的不断提交,创建新的线程来执行。创建源码:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

CachedThreadPool 的核心线程数为 0 ,最大线程数是 Integer.MAX_VALUE,可见所有的线程都是非核心线程。如果线程池的线程 60 秒,没有执行任务则会被销毁。由于使用了 SynchronousQueue,所以当主线程通过 SynchronousQueue.offer(Runnable task) 提交任务到队列后会阻塞,如果线程池中有可用的线程,则会执行当前任务,如果没有则会创建一个新的线程来执任务。

可见,如果任务太多的话,依然会造成 OOM ,与 LinkedBlockingQueue 不同的是,LinkedBlockingQueue 是由于任务对象太多,导致 OOM,ThreadPoolExecutor 则是由于 线程数太多导致 OOM 。

如何获取线程池中的返回结果?

可以使用 executor.submit(futureTask);,提交一个 FutureTask。代码如下:

自定义的线程
public class ThreadPoolCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Random random = new Random();
        int number = random.nextInt(100);
        System.out.println(Thread.currentThread().getName() + ":" + number);
        return number;
    }
}

使用:
    public static void func5() throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 10,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
        int result = 0;
        for (int i = 0; i < 7; i++) {
            Callable<Integer> callable = new ThreadPoolCallable();
            FutureTask<Integer> futureTask = new FutureTask<>(callable);
            executor.submit(futureTask);
            result += futureTask.get();
        }
        executor.shutdown();

        System.out.println("result = " + result);
    }

执行结果:

pool-1-thread-1:41
pool-1-thread-2:83
pool-1-thread-1:15
pool-1-thread-2:21
pool-1-thread-1:26
pool-1-thread-2:38
pool-1-thread-1:98
result = 322

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。未经许可不得转载!
本文链接:https://zdran.com/20200909.html