Java多线程-线程的概念和创建

原文地址 Java多线程-线程的概念和创建

前言

声明:该文章中所有测试都是在 JDK1.8 的环境下。

该文章是我在学习 Java 中的多线程这方面知识时,做的一些总结和记录。

如果有不正确的地方请大家多多包涵并作出指点,谢谢!

本文章有些内容参考并采用以下作品内容:

https://www.cnblogs.com/vipst...

https://www.bilibili.com/vide...

一、基础概念

我们知道 CPU 执行代码都是一条一条顺序执行的,所以本质上单核 CPU 的电脑不会在同一个时间点执行多个任务。但是在现实中,我们在使用电脑的时候,可以一边聊微信,一边听歌。那这是怎么做到的呢?其实操作系统多任务就是让 CPU 对多个任务轮流交替执行。

举个例子:在一个教室中同时坐着一年级,二年级,三年级三批学生,老师花一分钟教一年级,花一分教二年级,花一分钟教三年级,这样轮流下去,看上去就像同时在教三个年级。

同样的,我们使用电脑时,一边聊微信,一边听歌也是这个原理。CPU 让微信执行 0.001 秒,让音乐播放器执行 0.001 秒,在我们看来,CPU 就是在同时执行多个任务。

1.1 程序、进程和线程的概念

程序:被存储在磁盘或其他的数据存储设备中的可执行文件,也就是一堆静态的代码。

进程:运行在内存中可执行程序实例

线程:线程是进程的一个实体,是 CPU 调度和分派的基本单位。

看着这些概念是不是很抽象,看的很不舒服,那么下面我来用实例解释一下以上几个概念。

1.2 程序的运行实例

上面说到,我们使用电脑时,可以一边聊微信,一边听歌。那这些软件运行的整个过程是怎样的呢?

  • 如果我们要用微信聊天,大部分的人都是双击击桌面上的 "微信" 快捷方式,而双击这个快捷方式打开的实际上是一个. exe 文件,这个. exe 文件就是一个程序,请看下图:

  • 双击. exe 文件后,加载可执行程序到内存中,方便 CPU 读取,那这个可执行文件程序实例就是一个进程。请看下图:

  • 而我们在使用微信的时候,微信会做很多事情,比如加载微信 UI 界面,显示微信好友列表,与好友聊天,这些可以看成微信进程中一个个单独的线程。

我用一张图来总结一下整个过程:

根据上面内容对于线程概念的了解,是否有个疑问,线程是怎么创建出来的?带着这个疑问我们就来学习一下 java 中的线程是怎么如何创建的。

二、线程的创建

2.1 Thread 类的概念

java.lang.Thread 类代表线程,任何线程都是 Thread 类(子类)的实例。

2.2 常用的方法

构造方法功能介绍
Thread()使用无参的方式构造对象
Thread(String name)根据参数指定的名称来构造对象
Thread(Runnable target)根据参数指定的引用来构造对象,其中 Runnable 是个接口类型
Thread(Runnable target, String name)根据参数指定引用和名称来构造对象
成员方法功能介绍
run()1. 使用 Runnable 引用构造线程对象,调用该方法时最终调用接口中的版本
2. 没有使用 Runnable 引用构造线程对象,调用该方法时则啥也不做
start()用于启动线程,Java 虚拟机会自动调用该线程的 run 方法

2.3 创建方式

2.3.1 自定义 Thread 类创建

自定义类继承 Thread 类并根据自己的需求重写 run 方法,然后在主类中创建该类的对象调用 start 方法,这样就启动了一个线程。

示例代码如下:

//创建一个自定义类SubThreadRun继承Thread类,作为一个可以备用的线程类
public class SubThreadRun extends Thread {
    @Override
    public void run() {
        //打印1~20的整数值
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("SubThreadRun线程中:" + i);
        }
    }
}

//在主方法中创建该线程并启动
public class SubThreadRunTest {

    public static void main(String[] args) {

        //1.申明Thread类型的引用指向子类类型的对象
        Thread t1 = new SubThreadRun();
        //用于启动线程,java虚拟机会自动调用该线程中的run方法
        t1.start();
    }
}


输出结果:
    SubThreadRun线程中:0
    SubThreadRun线程中:2
    。。。。。。
    SubThreadRun线程中:19


到这里大家会不会有以下一个疑问,看示例代码:

public class SubThreadRunTest {

    public static void main(String[] args) {

        //1.申明Thread类型的引用指向子类类型的对象
        Thread t1 = new SubThreadRun();
        //调用run方法测试
        t1.run();
    }
}

输出结果:
    SubThreadRun线程中:0
    SubThreadRun线程中:1
    。。。。。。
    SubThreadRun线程中:19

我们不调用 start 方法,而是直接调用 run 方法,发现结果和调用 start 方法一样,他们两个方法的区别是啥呢?

我们在主方法中也加入一个打印 1-20 的数,然后分别用 run 方法和 start 方法进行测试,实例代码如下:

//使用run方法进行测试
public class SubThreadRunTest {

    public static void main(String[] args) {

        //1.申明Thread类型的引用指向子类类型的对象
        Thread t1 = new SubThreadRun();
        //2.调用run方法测试
        t1.run();

        //打印1~20的整数值
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("-----mian-----方法中:" + i);
        }
    }
}

输出结果:
    SubThreadRun线程中:0
    SubThreadRun线程中:1
    。。。。。。//都是SubThreadRun线程中
    SubThreadRun线程中:19
    -----mian-----方法中:0
    -----mian-----方法中:1
    。。。。。。//都是-----mian-----方法中
    -----mian-----方法中:19
     
    
//使用start方法进行测试
public class SubThreadRunTest {

    public static void main(String[] args) {

        //1.申明Thread类型的引用指向子类类型的对象
        Thread t1 = new SubThreadRun();
        //用于启动线程,java虚拟机会自动调用该线程中的run方法
        t1.start();

        //打印1~20的整数值
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("-----mian-----方法中:" + i);
        }
    }
}

输出结果:
    SubThreadRun线程中:0
    -----mian-----方法中:0
    SubThreadRun线程中:1
    SubThreadRun线程中:2
    -----mian-----方法中:1
    。。。。。。//SubThreadRun线程和mian方法相互穿插
    SubThreadRun线程中:19
    -----mian-----方法中:19

从上面的例子可知:

  • 调用 run 方法测试时,本质上就是相当于对普通成员方法的调用,因此执行流程就是 run 方法的代码执行完毕后才能继续向下执行。
  • 调用 start 方法测试时,相当于又启动了一个线程,加上执行 main 方法的线程,一共有两个线程,这两个线程同时运行,所以输出结果是交错的。(现在回过头来想想,现在是不是有点理解我可以用 qq 音乐一边听歌,一边在打字评论了呢。)

第一种创建线程的方式我们已经学会了,那这种创建线程的方式有没有什么缺陷呢?假如 SubThreadRun 类已经继承了一个父类,这个时候我们又要把该类作为自定义线程类,如果还是用继承 Thread 类的方法来实现的话就违背了 Java 不可多继承的概念。所以第二种创建方式就可以避免这种问题。

2.3.2 通过实现 Runnable 接口实现创建

自定义类实现 Runnable 接口并重写 run 方法,创建该类的对象作为实参来构造 Thread 类型的对象,然后使用 Thread 类型的对象调用 start 方法。

示例代码如下:

//创建一个自定义类SubRunnableRun实现Runnable接口
public class SubRunnableRun implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("SubRunnableRun线程中:" + i);
        }
    }
}

//在主方法中创建该线程并启动
public class SunRunnableRunTest {

    public static void main(String[] args) {

        //1.创建自定义类型的对象
        SubRunnableRun srr = new SubRunnableRun();
        //2.使用该对象作为实参构造Thread类型的对象
        Thread t1 = new Thread(srr);
        //3.使用Thread类型的对象调用start方法
        t1.start();

        //打印1~20之间的所有整数
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("-----mian-----方法中:" + i);
        }
    }
}

输出结果:
    SubRunnableRun线程中:0
    -----mian-----方法中:0
    SubRunnableRun线程中:1
    SubRunnableRun线程中:2
    -----mian-----方法中:1
    。。。。。。//SubRunnableRun线程和mian方法相互穿插
    SubRunnableRun线程中:19
    -----mian-----方法中:19

到这里大家会不会有一个疑问呢?

我在 SunRunnableRunTest 类的 main 方法中也实例化了 Thread 类,为什么该线程调用的是实现了 Runnable 接口的 SubRunnableRun 类中的 run 方法,而不是 Thread 类中的 run 方法。

为了解决该疑问,我们就进入 Thread 类去看一下源码,源码调用过程如下:

  1. 从上面的 SunRunnableRunTest 类中代码可知,我们创建线程调用的是 Thread 的有参构造方法,参数是 Runnable 类型的。

  2. 进入到 Thread 类中找到该有参构造方法,看到该构造方法调用 init 方法,并且把 target 参数继续当参数传递过去。

  3. 转到对应的 init 方法后,发现该 init 方法继续调用另一个重载的 init 方法,并且把 target 参数继续当参数传递过去。

  4. 继续进入到重载的 init 方法中,我们发现,该方法中把参数中 target 赋值给成员变量 target。

  5. 然后找到 Thread 类中的 run 方法,发现只要 Thread 的成员变量 target 存在,就调用 target 中的 run 方法。

通过查看源码,我们可以知道为什么我们创建的 Thread 类调用的是 Runnable 类中的 run 方法。

2.3.3 匿名内部类的方式实现创建

上面两种创建线程的方式都需要单独创建一个类来继承 Thread 类或者实现 Runnable 接口,并重写 run 方法。而匿名内部类可以不创建单独的类而实现自定义线程的创建。

示例代码如下:

public class ThreadNoNameTest {

    public static void main(String[] args) {

        //匿名内部类的语法格式:父类/接口类型 引用变量 = new 父类/接口类型 {方法的重写};
        //1.使用继承加匿名内部类的方式创建并启动线程
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("继承Thread类方式创建线程...");
            }
        };
        t1.start();
       
        //2.使用接口加匿名内部类的方式创建并启动线程
        Runnable ra = new Runnable() {
            @Override
            public void run() {
                System.out.println("实现Runnable接口方式实现线程...");
            }
        };
        Thread t2 = new Thread(ra);
        t2.start();
    }
}

这两个利用匿名内部类创建线程的方式还能继续简化代码,尤其是使用 Runnable 接口创建线程的方式,可以使用 Lambda 表达式进行简化。

示例代码如下:

public class ThreadNoNameTest {

    public static void main(String[] args) {

        //1.使用继承加匿名内部类简化后的方式创建并启动线程
        new Thread() {
            @Override
            public void run() {
                System.out.println("简化后继承Thread类方式创建线程...");
            }
        }.start();

        //2.使用接口加匿名内部类简化后的方式创建并启动线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("简化后实现Runnable接口方式实现线程...");
            }
        }).start();
        
        //2-1.对于接口加匿名内部创建线程,可以继续使用lambda表达式进行简化。
        //Java8开始支持lambda表达式:(形参列表) -> {方法体;}
        Runnable ra = () -> System.out.println("lambda表达式简化实现Runnable接口方式实现线程...");
        new Thread(ra).start();
        
        //继续简化
        new Thread(() -> System.out.println("lambda表达式简化实现Runnable接口方式实现线程...")).start();
    }
}

2.3.4 通过实现 Callable 接口创建

通过上面几个例子,我们了解了两种创建线程的方式,但是这两种方式创建线程存在一个问题,就是 run 方法是没有返回值的,所以如果我们希望在线程结束之后给出一个结果,那就需要用到实现 Callable 接口创建线程。

(1)Callable 接口

从 Java5 开始新增创建线程的第三中方式为实现 java.util.concurrent.Callable 接口。

常用方法如下:

成员方法功能介绍
V call()计算结果,如果无法计算结果,则抛出一个异常

我们知道启动线程只有创建一个 Thread 类并调用 start 方法,如果想让线程启动时调用到 Callable 接口中的 call 方法,就得用到 FutureTask 类。

(2)FutureTask 类

java.util.concurrent.FutureTask 类实现了 RunnableFuture 接口,RunnableFuture 接口是 Runnable 和 Future 的综合体,作为一个 Future,FutureTask 可以执行异步计算,可以查看异步程序是否执行完毕,并且可以开始和取消程序,并取得程序最终的执行结果,也可以用于获取调用方法后的返回结果。

常用方法如下:

构造方法功能介绍
FutureTask(Callable<v> callable)创建一个 FutureTask,它将在运行时执行给定的 Callable
成员方法功能介绍
V get()获取 call 方法计算的结果

从上面的概念可以了解到 FutureTask 类的一个构造方法是以 Callable 为参数的,然后 FutureTask 类是 Runnable 的子类,所以 FutureTask 类可以作为 Thread 类的参数。这样的话我们就可以创建一个线程并调用 Callable 接口中的 call 方法。

实例代码如下:

public class ThreadCallableTest implements Callable {
    @Override
    public Object call() throws Exception {
        //计算1 ~ 10000之间的累加和并打印返回
        int sum = 0;
        for (int i = 0; i <= 10000; i ++) {
            sum += i;
        }
        System.out.println("call方法中的结果:" + sum);
        return sum;
    }

    public static void main(String[] args) {

        ThreadCallableTest tct = new ThreadCallableTest();
        FutureTask ft = new FutureTask(tct);
        Thread t1 = new Thread(ft);
        t1.start();
        Object ob = null;
        try {
            ob = ft.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("main方法中的结果:" + ob);
    }
}

输出结果:
    call方法中的结果:50005000
    main方法中的结果:50005000

2.3.5 线程池的创建

线程池的由来

在讲线程池之前,先来讲一个故事,一个老板开个饭店,但这个饭店很奇怪,每来一个顾客,老板就去招一个新的大厨来做菜,等这个顾客走后,老板直接把这个大厨辞了。如果是按这种经营方式的话,老板每天就忙着招大厨,啥事都干不了。

对于上面讲的这个故事,我们现实生活中的饭店老板可没有这么蠢,他们都是在开店前就直接招了好几个大厨候在厨房,等有顾客来了,直接做菜上菜,顾客走后,厨师留在后厨待命,这样就把老板解放了。

现在我们来讲一下线程池的由来:比如说服务器编程中,如果为每一个客户都分配一个新的工作线程,并且当工作线程与客户通信结束时,这个线程被销毁,这就需要频繁的创建和销毁工作线程。如果访问服务器的客户端过多,那么会严重影响服务器的性能。

那么我们该如何解放服务器呢?对了,就像上面讲的饭店老板一样,打造一个后厨,让厨师候着。相对于服务器来说,就创建一个线程池,让线程候着,等待客户端的连接,等客户端结束通信后,服务器不关闭该线程,而是返回到线程中待命。这样就解放了服务器。

线程池的概念

首先创建一些线程,他们的集合称为线程池,当服务器接收到一个客户请求后,就从线程池中取出一个空余的线程为之服务,服务完后不关闭线程,而是将线程放回到线程池中。

相关类和方法

  • 线程池的创建方法总共有 7 种,但总体来说可分为 2 类:

  • Executors 是一个工具类和线程池的工厂类,用于创建并返回不同类型的线程池,常用的方法如下:

    返回值方法功能介绍
    static ExecutorServicenewFixedThreadPool(int nThreads)创建一个固定大小的线程池,如果任务数超出线程数,则超出的部分会在队列中等待
    static ExecutorServicenewCachedThreadPool()创建一个可已根据需要创建新线程的线程池,如果同一时间的任务数大于线程数,则可以根据任务数创建新线程,如果任务执行完成,则缓存一段时间后线程被回收。
    static ExecutorServicenewSingleThreadExecutor()创建单个线程数的线程池,可以保证先进先出的执行顺序
    static ScheduledExecutorServicenewScheduledThreadPool(int corePoolSize)创建一个可以执行延迟任务的线程池
    static ScheduledExecutorServicenewSingleThreadScheduledExecutor()创建一个单线程的可以执行延迟任务的线程池
    static ExecutorServicenewWorkStealingPool()创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】
  • ThreadPoolExecutor 通过构造方法创建线程池,最多可以设置 7 个参数,创建线程池的构造方法如下:

    构造方法功能介绍
    ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)通过最原始的方法创建线程池
  • 通过上面两类方法创建完线程池后都可以用 ExecutorService 接口进行接收,它是真正的线程池接口,主要实现类是 ThreadPoolExecutor,常用方法如下:

    方法声明功能介绍
    void execute(Runnable command)执行任务和命令,通常用于执行 Runnable
    <T> Future<T> submit(Callable<T> task)执行任务和命令,通常用于执行 Callable
    void shutdown()启动有序关闭

代码实例

  1. 使用 newFixedThreadPool 方法创建线程池

    public class FixedThreadPool {
        public static void main(String[] args) {
            
            // 创建含有两个线程的线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(2);
            // 创建任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程:" + Thread.currentThread().getName() + "执行了任务!");
                }
            };
            // 线程池执行任务
            threadPool.execute(runnable);
            threadPool.execute(runnable);
            threadPool.execute(runnable);
            threadPool.execute(runnable);
        }
    }
    
    输出结果:
        线程:pool-1-thread-2执行了任务!
        线程:pool-1-thread-1执行了任务!
        线程:pool-1-thread-2执行了任务!
        线程:pool-1-thread-1执行了任务!
    
    

从结果上可以看出,这四个任务分别被线程池中的固定的两个线程所执行,线程池也不会创建新的线程来执行任务。

  1. 使用 newCachedThreadPool 方法创建线程池

    public class cachedThreadPool {
        public static void main(String[] args) {
    
            //1.创建线程池
            ExecutorService executorService = Executors.newCachedThreadPool();
            //2.设置任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程:" + Thread.currentThread().getName() + "执行了任务!");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                    }
                }
            };
            //3.执行任务
            for (int i = 0; i < 100; i ++) {
                executorService.execute(runnable);
            }
        }
    }
    
    输出结果:
        线程:pool-1-thread-1执行了任务!
        线程:pool-1-thread-4执行了任务!
        线程:pool-1-thread-3执行了任务!
        线程:pool-1-thread-2执行了任务!
        线程:pool-1-thread-5执行了任务!
        线程:pool-1-thread-7执行了任务!
        线程:pool-1-thread-6执行了任务!
        线程:pool-1-thread-8执行了任务!
        线程:pool-1-thread-9执行了任务!
        线程:pool-1-thread-10执行了任务!
    
    

从结果上可以看出,线程池根据任务的数量来创建对应的线程数量。

  1. 使用 newSingleThreadExecutor 的方法创建线程池

    public class singleThreadExecutor {
    
        public static void main(String[] args) {
    
            //创建线程池
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            //执行任务
            for (int i = 0; i < 10; i ++) {
                final int task = i + 1;
                executorService.execute(()->{
                    System.out.println("线程:" + Thread.currentThread().getName() + "执行了第" + task +"任务!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }
    
    输出结果:
        线程:pool-1-thread-1执行了第1任务!
        线程:pool-1-thread-1执行了第2任务!
        线程:pool-1-thread-1执行了第3任务!
        线程:pool-1-thread-1执行了第4任务!
        线程:pool-1-thread-1执行了第5任务!
        线程:pool-1-thread-1执行了第6任务!
        线程:pool-1-thread-1执行了第7任务!
        线程:pool-1-thread-1执行了第8任务!
        线程:pool-1-thread-1执行了第9任务!
        线程:pool-1-thread-1执行了第10任务!
    
    

从结果可以看出,该方法创建的线程可以保证任务执行的顺序。

  1. 使用 newScheduledThreadPool 的方法创建线程池

    public class ScheduledThreadPool {
    
        public static void main(String[] args) {
    
            //创建包含2个线程的线程池
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
            //记录创建任务时的当前时间
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date startTime = new Date();
            String start = formatter.format(startTime);
            System.out.println("创建任务时的时间:" + start);
            //创建任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    Date endTime = new Date();
                    String end = formatter.format(endTime);
                    System.out.println("线程:" + Thread.currentThread().getName() + "任务执行的时间为:" + end);
                }
            };
            //执行任务(参数:runnable-要执行的任务,2-从现在开始延迟执行的时间,TimeUnit.SECONDS-延迟参数的时间单位)
            for(int i = 0; i < 2; i ++) {
                scheduledExecutorService.schedule(runnable,2, TimeUnit.SECONDS);
            }
        }
    }
    
    输出结果:
        创建任务的时间:2021-04-19 19:26:18
        线程:pool-1-thread-1任务执行的时间为:2021-04-19 19:26:20
        线程:pool-1-thread-2任务执行的时间为:2021-04-19 19:26:20
    
    

从结果可以看出,该方法创建的线程池可以分配已有的线程执行一些需要延迟的任务。

  1. 使用 newSingleThreadScheduledExecutor 方法创建线程池

    public class SingleThreadScheduledExecutor {
    
        public static void main(String[] args) {
    
            //创建线程池
            ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
            //创建任务
            Date startTime = new Date();
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String start = formatter.format(startTime);
            System.out.println("创建任务的时间:" + start);
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    Date endTime = new Date();
                    String end = formatter.format(endTime);
                    System.out.println("线程:" + Thread.currentThread().getName() + "任务执行的时间为:" + end);
                }
            };
            //执行任务
            for(int i = 0; i < 2; i ++) {
                scheduledExecutorService.schedule(runnable,2, TimeUnit.SECONDS);
            }
        }
    }
    
    输出结果:
        创建任务的时间:2021-04-19 19:51:58
        线程:pool-1-thread-1任务执行的时间为:2021-04-19 19:52:00
        线程:pool-1-thread-1任务执行的时间为:2021-04-19 19:52:00
    
    

从结果可以看出,该方法创建的线程池只有一个线程,该线程去执行一些需要延迟的任务。

  1. 使用 newWorkStealingPool 方法创建线程池

    public class newWorkStealingPool {
    
        public static void main(String[] args) {
    
            //创建线程池
            ExecutorService executorService = Executors.newWorkStealingPool();
            //执行任务
            for (int i = 0; i < 4; i ++) {
                final int task = i + 1;
                executorService.execute(()->{
                    System.out.println("线程:" + Thread.currentThread().getName() + "执行了第" + task +"任务!");
                });
            }
            //确保任务被执行
            while (!executorService.isTerminated()) {
            }
        }
    }
    
    输出结果:
        线程:ForkJoinPool-1-worker-9执行了第1任务!
        线程:ForkJoinPool-1-worker-4执行了第4任务!
        线程:ForkJoinPool-1-worker-11执行了第3任务!
        线程:ForkJoinPool-1-worker-2执行了第2任务!
    
    

从结果可以看出,该方法会创建一个含有足够多线程的线程池,来维持相应的并行级别,任务会被抢占式执行。(任务执行顺序不确定)

  1. 使用 ThreadPoolExecutor 创建线程池

    在编写示例代码之前我先来讲一个生活的例子(去银行办理业务):

    描述业务场景:银行一共有 4 个窗口,今天只开放两个,然后等候区一共 3 个位置。如下图所示:

    • 如果银行同时办理业务的人小于等于 5 个人,那么正好,2 个人先办理,其他的人在等候区等待。如下图所示:

    • 如果银行同时办理业务的人等于 6 个人时,银行会开放三号窗口来办理业务。如下图所示:

    • 如果银行同时办理业务的人等于 7 个人时,银行会开放四号窗口来办理业务。如下图所示:

    • 如果银行同时办理业务的人大于 7 个人时,则银行大厅经理就会告诉后面的人,该网点业务已满,请去其他网点办理。

    现在我们再来看一下我们的 ThreadPoolExecutor 构造方法,该构造方法最多可以设置 7 个参数:

    ThreadPoolExecutor(int corePoolSize, 
                       int maximumPoolSize, 
                       long keepAliveTime, 
                       TimeUnit unit, 
                       BlockingQueue<Runnable> workQueue, 
                       ThreadFactory threadFactory, 
                       RejectedExecutionHandler handler)
    
    

参数介绍:

  1. corePoolSize:核心线程数,在线程池中一直存在的线程(对应银行办理业务模型:一开始就开放的窗口)

  2. maximumPoolSize:最大线程数,线程池中能创建最多的线程数,除了核心线程数以外的几个线程会在线程池的任务队列满了之后创建(对应银行办理业务模型:所有窗口)

  3. keepAliveTime:最大线程数的存活时间,当长时间没有任务时,线程池会销毁一部分线程,保留核心线程

  4. unit:时间单位,是第三个参数的单位,这两个参数组合成最大线程数的存活时间

    • TimeUnit.DAYS:天
    • TimeUnit.HOURS:小时
    • TimeUnit.MINUTES:分
    • TimeUnit.SECONDS:秒
    • TimeUnit.MILLISECONDS:毫秒
    • TimeUnit.MICROSECONDS:微秒
    • TimeUnit.NANOSECONDS:纳秒
  5. workQueue:等待队列,用于保存在线程池等待的任务(对应银行办理业务模型:等待区)

    • ArrayBlockingQueue:一个由数组支持的有界阻塞队列。
    • LinkedBlockingQueue:一个由链表组成的有界阻塞队列。
    • SynchronousQueue:该阻塞队列不储存任务,直接提交给线程,这样就会形成对于提交的任务,如果有空闲线程,则使用空闲线程来处理,否则新建一个线程来处理任务。
    • PriorityBlockingQueue:一个带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素
    • DelayQueue:一个使用优先级队列实现支持延时获取元素的无界阻塞队列,只有在延迟期满时才能从中提取元素,现实中的使用: 淘宝订单业务: 下单之后如果三十分钟之内没有付款就自动取消订单。
    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
  6. threadFactory:线程工厂,用于创建线程。

  7. handler:拒绝策略,任务超出线程池可接受范围时,拒绝处理任务时的策略。

    • ThreadPoolExecutor.AbortPolicy:当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常(默认使用该策略
    • ThreadPoolExecutor.CallerRunsPolicy:当任务添加到线程池中被拒绝时,会调用当前线程池的所在的线程去执行被拒绝的任务
    • ThreadPoolExecutor.DiscardOldestPolicy:当任务添加到线程池中被拒绝时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去
    • ThreadPoolExecutor.DiscardPolicy:如果该任务被拒绝,这直接忽略或者抛弃该任务

当任务数小于等于核心线程数 + 等待队列数量的总和时

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>执行任务");
            }
        };
        // 执行任务
        for (int i = 0; i < 5; i++) {
            threadPool.execute(runnable);
        }
        //关闭线程池
        threadPool.shutdown();
    }
}


输出结果:
    pool-1-thread-2==>执行任务
    pool-1-thread-1==>执行任务
    pool-1-thread-2==>执行任务
    pool-1-thread-1==>执行任务
    pool-1-thread-2==>执行任务

从结果中可以看出,只有两个核心线程在执行任务。

当任务数大于核心线程数 + 等待队列数量的总和,但是小于等于最大线程数时

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>执行任务");
            }
        };
        // 执行任务
        for (int i = 0; i < 7; i++) {
            threadPool.execute(runnable);
        }
        //关闭线程池
        threadPool.shutdown();
    }
}

输出结果:
    pool-1-thread-1==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-2==>执行任务
    pool-1-thread-2==>执行任务
    pool-1-thread-3==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-1==>执行任务

从结果中可以看出,启动了最大线程来执行任务。

当任务数大于最大线程数时

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>执行任务");
            }
        };
        // 执行任务
        for (int i = 0; i < 8; i++) {
            threadPool.execute(runnable);
        }
        //关闭线程池
        threadPool.shutdown();
    }
}

输出结果:
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.zck.task18.ThreadPool.ThreadPoolExecutorTest$1@7f31245a rejected from java.util.concurrent.ThreadPoolExecutor@6d6f6e28[Running, pool size = 4, active threads = 0, queued tasks = 0, completed tasks = 7]
    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.zck.task18.ThreadPool.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:25)
    pool-1-thread-1==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-4==>执行任务
    pool-1-thread-2==>执行任务
    pool-1-thread-3==>执行任务
    pool-1-thread-1==>执行任务


从结果中可以看出,任务大于最大线程数,使用拒绝策略直接抛出异常。

三、总结

本文介绍了三种线程的创建方式:

  • 自定义类继承 Thread 类并重写 run 方法创建
  • 自定义类实现 Runnable 接口并重写 run 方法创建
  • 实现 Callable 接口创建

介绍了七种线程池的创建方式:

  • 使用 newFixedThreadPool 方法创建线程池
  • 使用 newCachedThreadPool 方法创建线程池
  • 使用 newSingleThreadExecutor 的方法创建线程池
  • 使用 newScheduledThreadPool 的方法创建线程池
  • 使用 newSingleThreadScheduledExecutor 方法创建线程池
  • 使用 newWorkStealingPool 方法创建线程池
  • 使用 ThreadPoolExecutor 创建线程池
原文地址:https://www.cnblogs.com/hanlk/p/14721179.html