0
点赞
收藏
分享

微信扫一扫

高薪程序员&面试题精讲系列61之线程创建方式有哪些?线程池的构造方法有哪些核心参数?

灯火南山 2022-02-09 阅读 64

一. 面试题及剖析

1. 今日面试题

2. 题目剖析

上一篇文章中,壹哥 给大家介绍了进程、线程的内容,并重点分析了两者之间的区别。进程是不需要我们自己来创建的,但线程就得需要我们来创建了,那么有哪些创建线程的方式呢?而在多线程环境下,线程池是我们创建线程的首选,所以线程池的使用,自然也就成了我们面试时的一个重点。

那么接下来就请大家阅读本文,看看 壹哥 都给大家总结了哪些创建线程的方式吧。

二. 线程创建方式

1. 创建方式

在Java中,其实为创建线程提供了多种方式,这几种方式如下:

2. 继承Thread类的方式

2.1 基本介绍

我们学习线程时,创建线程的第一种方式就是通过继承Thread类来实现的,其创建过程如下所示:

2.2 代码示例

public class ThreadDemo01 extends Thread {

    /**
     * 重写run方法,run方法的方法体就是现场执行体
     */
    @Override
    public void run() {
        System.out.println("线程--" + getName());
    }

    public static void main(String[] args) {
        System.out.println("主线程: " + Thread.currentThread().getName());

        //开启一个新线程
        new ThreadDemo01().start();
    }

}

通过继承Thread实现的线程类,多个线程间无法共享线程类的实例变量。

3. 实现Runnable接口的方式

3.1 基本介绍

创建线程的另一种常用方式是通过实现Runnable接口来创建一个线程。

3.2 示例代码

public class ThreadDemo02 implements Runnable {

    /**
     * 重写run方法,run方法的方法体就是现场执行体
     */
    @Override
    public void run() {
        System.out.println("线程--" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        System.out.println("主线程: " + Thread.currentThread().getName());

        //开启一个新线程
        ThreadDemo02 demo02 = new ThreadDemo02();
        new Thread(demo02, "新线程01").start();
    }

}

通过实现Runnable接口的线程类,可以实现资源的互相共享。

4. 实现Callable接口的方式

4.1 基本介绍

接下来 壹哥 再给大家介绍第3种创建线程的方式,这种方式用的相对较少。

4.2 示例代码

public class ThreadDemo03 implements Callable<Integer> {

    public static void main(String[] args) {
        try {
            //开启一个新线程
            ThreadDemo03 demo03 = new ThreadDemo03();
            //创建FutureTask对象
            FutureTask<Integer> task = new FutureTask<>(demo03);
            //开启线程
            new Thread(task, "有返回值的线程").start();
            System.out.println("子线程的返回值:" + task.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Integer call() throws Exception {
        System.out.println("当前线程--" + Thread.currentThread().getName() + "--" + 0);
        return 0;
    }
}

Callable接口相当于是Runable接口的升级版,提供了call()方法将作为线程的执行体,同时允许有返回值,且可以抛出异常。

但Callable对象不能直接作为Thread对象的target,因为Callable接口是 Java 5 中新增的接口,不是Runnable接口的子接口。为了解决这个问题,就引入 Future接口,此接口可以接受call()方法的返回值。RunnableFuture接口是Future接口和Runnable接口的子接口,可以作为Thread对象的target,且Future 接口提供了一个实现类FutureTask。FutureTask实现了RunnableFuture接口,可以作为 Thread对象的target。

5. 前3种线程创建方式对比

前文中,壹哥 带大家复习了3种创建线程的方式,分别通过继承Thread类、实现Runnable与Callable接口的方式创建了线程时,我们先把这3种方式对比一下。

5.1 实现接口的优、缺点是:

5.2 继承Thread类的优、缺点是:

5. 使用匿名内部类的方式

有时候我们只想简单的创建一个线程,这时候也可以直接使用匿名内部类的方式来创建一个线程,代码如下所示:

public class ThreadDemo04 {

    public static void main(String[] args) {
        
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 线程需要执行的任务代码
                System.out.println("线程执行....");
                for (int i = 0; i < 10; i++) {
                    System.out.println("i=" + i);
                }
            }
        });

        //启动线程
        thread.start();
    }

}

6. 使用线程池创建的方式(重点)

我们可以利用Executor工具类来创建普通线程池,创建出来的线程池都实现了ExecutorService接口,有如下几种创建线程池的方式:

接下来 壹哥 就通过上述几种方式,来给大家展示如何通过线程池来创建线程。咱们话不要多,来直接撸!

6.1 newFiexedThreadPool()方法创建

该方式适用于为了满足资源管理需求,而需要限制当前线程数量的场合,尤其是负载比较重的服务器中。

public class ThreadDemo05 {

    public static void main(String[] args) {
        //方式1: 创建固定数目线程的线程池;
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> System.out.println("当前线程=" + Thread.currentThread().getName()));
        }

        //关闭线程池
        executorService.shutdown();
    }

}

6.2 newSingleThreadExecutor()方法创建

该方式适用于需要保证顺序执行各个任务的场景。

public static void main(String[] args) {
        //方式2: 创建固定数目线程的线程池;
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> System.out.println("当前线程=" + Thread.currentThread().getName()));
        }

        //关闭线程池
        executorService.shutdown();
    }

}

6.3 newCachedThreadPool()方法创建

public static void main(String[] args) {
        //方式3: 创建一个可缓存的线程池
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> System.out.println("当前线程=" + Thread.currentThread().getName()));
        }

        //关闭线程池
        executorService.shutdown();
    }

}

6.4 newScheduledThreadPool()方法创建

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

public static void main(String[] args) {
        //方式4: 创建一个支持定时及周期性的任务执行的线程池
        ExecutorService executorService = Executors.newScheduledThreadPool(5);

        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> System.out.println("当前线程=" + Thread.currentThread().getName()));
        }

        //关闭线程池
        executorService.shutdown();
    }

}

6.5 ThreadPoolExecutor创建(推荐)

虽然已经有了4种创建线程池的方式了,但 壹哥 要告诉你的是,上面4种创建线程池的方式,都不推荐使用!啥?!搞了半天,竟然都不是适用?闹啥呢?凭什么啊?请不要跟我急,这事真不是我说的!

在阿里巴巴的《Java开发手册》中提到,使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出),所以不推荐我们使用Executors来创建线程!

那为啥不推荐用Executors呢?这主要是因为newFixedThreadPool()方法的底层用到了LinkedBlockingQueue,我们来看看其源码:

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

BlockingQueue是一种阻塞队列接口,它有两个具体的实现,即ArrayBlockingQueue 和 LinkedBlockingQueue,两者作用如下:

而newFixedThreadPool()方法中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

同样的,在newSingleThreadExecutor()方法中也存在该问题。所以这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。

那么为了避免产生OOM,阿里的Java开发文档中,就推荐我们直接调用ThreadPoolExecutor的构造函数,来自己创建线程池。而且在创建线程池对象的同时,也需要给BlockQueue指定固定的容量。

public static void main(String[] args) {
    //方式5: 通过ThreadPoolExecutor的构造方法来创建
    ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue(10),Executors.defaultThreadFactory());

    for (int i = 0; i < 5; i++) {
        executor.execute(() -> System.out.println("当前线程=" + Thread.currentThread().getName()));
    }
    
    //关闭线程池
    executor.shutdown();

}

6.6 ThreadPoolExecutor核心参数(重点)

这里ThreadPoolExecutor构造方法的核心参数如下:

以上这几个核心参数,大家一定要记下来,这是面试线程池时经常问的一个知识点!

三. 结语

至此,壹哥 就给大家总结了5种创建线程的方式,现在你都记住了吗?你觉得哪种方式更好用呢?请在评论区留言讨论吧。

举报

相关推荐

0 条评论