写点什么

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

用户头像
U+2647
关注
发布于: 2021 年 04 月 07 日

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 = 0run pool-1-thread-3, number = 3run pool-1-thread-2, number = 1Exception 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 = 0run main, number = 4run pool-1-thread-3, number = 3run pool-1-thread-2, number = 1run main, number = 5run pool-1-thread-3, number = 2run 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 = 1run pool-1-thread-3, number = 3run pool-1-thread-1, number = 0run 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 = 0run pool-1-thread-3, number = 3run pool-1-thread-2, number = 1run 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 = 1run pool-1-thread-3, number = 3run pool-1-thread-1, number = 0java.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:41pool-1-thread-2:83pool-1-thread-1:15pool-1-thread-2:21pool-1-thread-1:26pool-1-thread-2:38pool-1-thread-1:98result = 322
复制代码


发布于: 2021 年 04 月 07 日阅读数: 21
用户头像

U+2647

关注

evolving code monkey 2018.11.05 加入

https://zdran.com/

评论

发布
暂无评论
面试笔记(二)线程池连环炮