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

前言

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

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

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

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

https://www.cnblogs.com/vipstone/p/14149065.html

https://www.bilibili.com/video/BV1dt4y1i7Gt?t=1056

一、基础概念

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

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

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

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

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

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

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

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

1.2 程序的运行实例

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

  • 如果我们要用微信聊天,大部分的人都是双击击桌面上的"微信"快捷方式,而双击这个快捷方式打开的实际上是一个.exe文件,这个.exe文件就是一个程序,请看下图:
image-20210311152434762.png
  • 双击.exe文件后,加载可执行程序到内存中,方便CPU读取,那这个可执行文件程序实例就是一个进程。请看下图:
image-20210311160217702.png
  • 而我们在使用微信的时候,微信会做很多事情,比如加载微信UI界面,显示微信好友列表,与好友聊天,这些可以看成微信进程中一个个单独的线程。

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

程序执行流程.jpg

根据上面内容对于线程概念的了解,是否有个疑问,线程是怎么创建出来的?带着这个疑问我们就来学习一下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引用构造线程对象,调用该方法时最终调用接口中的版本<br />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类型的。

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

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

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

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

    image-20210319160341523.png

通过查看源码,我们可以知道为什么我们创建的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 类:
image-20210418203505283.png
  • Executors是一个工具类和线程池的工厂类,用于创建并返回不同类型的线程池,常用的方法如下:

    返回值 方法 功能介绍
    static ExecutorService newFixedThreadPool(int nThreads) 创建一个固定大小的线程池,如果任务数超出线程数,则超出的部分会在队列中等待
    static ExecutorService newCachedThreadPool() 创建一个可已根据需要创建新线程的线程池,如果同一时间的任务数大于线程数,则可以根据任务数创建新线程,如果任务执行完成,则缓存一段时间后线程被回收。
    static ExecutorService newSingleThreadExecutor() 创建单个线程数的线程池,可以保证先进先出的执行顺序
    static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个可以执行延迟任务的线程池
    static ScheduledExecutorService newSingleThreadScheduledExecutor() 创建一个单线程的可以执行延迟任务的线程池
    static ExecutorService newWorkStealingPool() 创建一个抢占式执行的线程池(任务执行顺序不确定)【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执行了任务!
    

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

  2. 使用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执行了任务!
    

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

  3. 使用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任务!
    

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

  4. 使用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
    

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

  5. 使用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
    

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

  6. 使用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任务!
    

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

  7. 使用ThreadPoolExecutor创建线程池

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

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

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

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

    银行实例3.jpg
    • 如果银行同时办理业务的人等于7个人时,银行会开放四号窗口来办理业务。如下图所示:
    银行实例4.jpg
    • 如果银行同时办理业务的人大于7个人时,则银行大厅经理就会告诉后面的人,该网点业务已满,请去其他网点办理。

      银行实例5.jpg

    现在我们再来看一下我们的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创建线程池
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容