【Java JUC】Java多线程并发编程零基础小白入门(上)

前言

在被面试官疯狂打击后,我决定重新系统学习JUC编程,也就是并发编程。本篇文章作为Java语言多线程并发编程的入门级别,仅从学会使用的角度学习,并不深入涉及有关知识的底层实现原理,比如线程池机制、synchronized关键字、ReentrantLock等。文章会穿插一些面试会问到的面试题。

0、预备知识

在学习Java的多线程之前,首先我们需要对操作系统并发相关的理论知识有所了解,在这里呢就简单说几点重要的。有关操作系统进程线程模型的详细知识看这里:

下面我就以面试题的形式,列举一些并发相关的基础问题。

1. 说说什么是并发和并行,有什么区别?

在操作系统中,并发(Concurrent),是指在一个时间段中多个进程指令被快速的在CPU上轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行,然而在同一时刻仍只有一个进程的一条指令在CPU执行。一般发生在单CPU单核的机器上。

并行(Parallel),是指当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以在同一时刻真正做到同时进行,这种方式我们称之为并行。也就是说同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

简单来说,并发是指在一段时间内宏观上多个程序同时运行。并行指的是同一个时刻,多个任务确实真的在同时运行。

需要注意的是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的同时进行

2. 什么是进程,什么是线程,有什么区别和联系

进程是一个静态程序的运行实例,简单说就是正在执行的程序,并且是操作系统进行资源分配的一个基本单位。

线程是进程的一个实体,线程是比进程更小的能独立运行的基本单位,也是 CPU 调度和分派的基本单位,创建、销毁、切换成本要小于进程,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性,有时又被称为轻权进程或轻量级进程。

进程必须至少包含一个线程,线程不能独立存在,必须归属于一个进程。

二者的区别如下:

  1. 进程是资源分配的最小单位,而线程是 CPU 调度的最小单位;

  2. 创建进程或撤销进程,系统都要为之分配或回收资源,操作系统开销远大于创建或撤销线程时的开销;

  3. 不同进程地址空间相互独立,同一进程内的线程共享同一地址空间。一个进程的线程在另一个进程内是不可见的;

  4. 进程间不会相互影响,而一个线程挂掉将可能导致整个进程挂掉;

JAVA

从Java虚拟机的角度来理解:Java虚拟机的运行时数据区包含堆、方法区、虚拟机栈、本地方法栈、程序计数器。各个进程之间是相互独立的,每个进程会包含多个线程,每个进程所包含的多个线程并不是相互独立的,这个线程会共享进程的堆和方法区,但这些线程不会共享虚拟机栈、本地方法栈、程序计数器。即每个进程所包含的多个线程共享进程的堆和方法区,并且具备私有的虚拟机栈、本地方法栈、程序计数器。

二者主要区别在于内存分配和资源开销:

  • 内存分配:进程之间的地址空间和资源是相互独立的,同一个进程之间的线程会共享线程的地址空间和资源(堆和方法区)。

  • 资源开销:每个进程具备各自的数据空间,进程之间的切换会有较大的开销。属于同一进程的线程会共享堆和方法区,同时具备私有的虚拟机栈、本地方法栈、程序计数器,线程之间的切换资源开销较小。

3. 为什么有了进程,还要有线程呢?

简单说就是为了提高操作系统的并发性能。

进程可以使多个程序并发执行,以提高资源的利用率和系统的吞吐量,但是其带来了一些缺点:

  1. 进程在同一时间只能干一件事情;

  2. 进程在执行的过程中如果阻塞,整个进程就会被挂起,即使进程中有些工作不依赖与等待的资源,仍然不会执行;

  3. 进程之间的资源开销比较大。

基于以上的缺点,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时间和空间开销,提高并发性能。

4. 为什么使用多线程,会带来什么问题?

之所以使用多线程,目的就是为了提高操作系统并发量,当一个线程进入等待状态或者阻塞时,CPU可以先去执行其他线程,提高CPU的利用率。

但是会带来一些问题,比如:

  • 频繁的上下文切换会影响多线程的执行速度;

  • 线程的死锁问题;

  • 资源限制:在进行并发编程时,程序的执行速度受限于计算机的硬件或软件资源。在并发编程中,程序执行变快的原因是将程序中串行执行的部分变成并发执行,如果因为资源限制,并发执行的部分仍在串行执行,程序执行将会变得更慢,因为程序并发需要上下文切换和资源调度。

5. 简单说说线程的上下文切换。

当一个线程的时间片用完后或者其他自身原因被迫暂停运行了,这时候,另外一个线程或者进程或者其他进程的线程就会被操作系统选中,用来占用处理器。这种正在执行的一个线程被暂停,另一个线程被选中开始执行的过程就会引发线程的上下文切换。

  • 自发性上下文切换是指线程由 Java 程序调用导致切出,在多线程编程中,执行调用sleep、wait、yeild、join等方法或lock、synchronized 关键字,常常就会引发自发性上下文切换。

  • 非自发性上下文切换指线程由于调度器的原因被迫切出。常见的有:线程被分配的时间片用完,虚拟机垃圾回收导致或者执行优先级的问题导致。

一个线程让出处理器使用权,就是“切出”;另外一个线程获取处理器使用权。就是“切入”,在这个切入切出的过程中,操作系统会保存和恢复相关的进度信息,这个进度信息就是我们常说的“上下文”,上下文中一般包含了寄存器的存储内容以及程序计数器存储的指令内容。

  • CPU寄存器负责存储已经、正在和即将要执行的任务。

  • 程序计数器负责寄存CPU正在执行的指令位置和即将执行的下一条指令的位置。

6. Java中的守护线程和用户线程有什么区别?

Java 语言中线程分为两类:用户线程和守护线程。Java 语言中无论是线程还是线程池,默认都是用户线程,因此用户线程也被称为普通线程。以线程为例,想要查看线程是否为守护线程只需通过调用 isDaemon() 方法查询即可,如果查询的值为 false 则表示不是守护线程,属于用户线程。

守护线程(Daemon Thread)也被称之为后台线程或服务线程,守护线程是为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束

那如何将默认的用户线程修改为守护线程呢? 这个问题要分为两种情况来回答,首先如果是线程,则可以通过设置 setDaemon(true) 方法将用户线程直接修改为守护线程,而如果是线程池则需要通过 ThreadFactory 将线程池中的每个线程都为守护线程才行,代码如下:

设置线程为守护线程 如果使用的是线程,可以通过 setDaemon(true) 方法将线程类型更改为守护线程,如下代码所示:

public static void main(String[] args) throws InterruptedException {
     Thread thread = new Thread(new Runnable() {
 @Override
 public void run() {
             System.out.println("我是子线程");
         }
     });
 // 设置子线程为守护线程
     thread.setDaemon(true);
     System.out.println("子线程==守护线程:" + thread.isDaemon());
     System.out.println("主线程==守护线程:" + Thread.currentThread().isDaemon());
 }

设置线程池为守护线程

要把线程池设置为守护线程相对来说麻烦一些,需要将线程池中的所有线程都设置成守护线程,这个时候就需要使用 ThreadFactory 来定义线程池中每个线程的线程类型了,具体实现代码如下:

// 创建固定个数的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10, new ThreadFactory() {
 @Override
 public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
 // 设置线程为守护线程
        t.setDaemon(false);
 return t;
    }
});

如果想在主线程结束后JVM进程马上结束,那么创建线程的时候可以设置线程为守护线程,否则如果希望主线程结束后子线程继续工作,等子线程结束后在让JVM进程结束那么就设置子线程为用户线程。

守护线程应用场景

守护线程的典型应用场景就是垃圾回收线程,当然还有一些场景也非常适合使用守护线程,比如服务器端的健康检测功能,对于一个服务器来说健康检测功能属于非核心非主流的服务业务,像这种为了主要业务服务的业务功能就非常合适使用守护线程,当程序中的主要业务都执行完成之后,服务业务也会跟随者一起销毁

总结:在 Java 语言中线程分为用户线程和守护线程,守护线程是用来为用户线程服务的,当一个程序中的所有用户线程都结束之后,无论守护线程是否在工作都会跟随用户线程一起结束。守护线程从业务逻辑层面来看权重比较低,但对于线程调度器来说无论是守护线程还是用户线程,在优先级相同的情况下被执行的概率都是相同的。守护线程的经典使用场景是垃圾回收线程,守护线程中创建的线程默认情况下也都是守护线程。

1、创建线程

1. Thread类

线程开启我们需要用到java.lang.Thread类,API中该类中定义了有关线程的一些方法,具体如下:

常用构造方法:

  • public Thread():分配一个新的线程对象。

  • public Thread(String name):分配一个指定名字的新的线程对象。

  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。

  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

通过观察Thread类的源码发现,class Thread implements Runnable,那么也就意味着我们可以这么写代码:

public class Demo {
    public static void main(String[] args) {
        Thread thread = new Thread(new ThreadDemo1(), "线程一");
        thread.start();

    }
}

class ThreadDemo1 extends Thread{
    @Override
    public void run() {
        System.out.println(getName() + "running.....");
    }
}

// Thread-0running.....

需要注意的是,这样写纯属是验证符合语法,编译不会报错,但是通过这种方式创建的线程给的名字无效!

常用方法:

  • public String getName():获取当前线程名称。

    • 线程存在默认名称,子线程的默认名称是:Thread-索引。

    • 主线程的默认名称就是:main

  • public void start():启动线程,使线程从NEW状态进入RUNNABLE状态; Java虚拟机调用此线程对象的run方法。

  • public void run():此线程要执行的任务在此处定义代码。

  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  • public static Thread currentThread():返回对当前正在执行的线程对象的引用。

    • 获取当前线程对象,这个代码在哪个线程中,就得到哪个线程对象。

翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,严格来说本质上是只有一种方式。

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。

Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。

  2. 创建Thread子类的实例,即创建了线程对象。

  3. 调用线程对象的start()方法来启动该线程。

代码如下:

public class ThreadDemo {
    // 启动后的ThreadDemo当成一个进程。
    // main方法是由主线程执行的,理解成main方法就是一个主线程
    public static void main(String[] args) {
        // 创建一个线程对象
        Thread t1 = new MyThread();
        t1.setName("1号线程");
        t1.start();
        //System.out.println(t1.getName()); // 获取线程名称

        Thread t2 = new MyThread();
        t2.setName("2号线程");
        t2.start();
        //System.out.println(t2.getName());  // 获取线程名称

        // 主线程的名称如何获取呢?
        // 这个代码在哪个线程中,就得到哪个线程对象。
        Thread m = Thread.currentThread();
        m.setName("最强线程main");
        //System.out.println(m.getName()); // 获取线程名称

        for(int i = 0 ; i < 10 ; i++ ){
            System.out.println(m.getName()+"==>"+i);
        }
    }
}

// 1.定义一个线程类继承Thread类。
class MyThread extends Thread{
    // 2.重写run()方法
    @Override
    public void run() {
        // 线程的执行方法。
        for(int i = 0 ; i < 10 ; i++ ){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
        }
    }
}

2. Runnable接口

采用实现java.lang.Runnable接口也是非常常见的一种,我们只需要重写run方法即可。

步骤如下:

  1. 定义Runnable接口的实现类(也就是定义一个线程任务类),并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

  2. 创建Runnable实现类的实例(也就是创建一个任务类对象),并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

  3. 调用线程对象的start()方法来启动线程。

代码如下:

public class ThreadDemo {
    public static void main(String[] args) {
        // 3.创建一个自定义线程任务类对象(注意:线程任务对象不是线程对象,只是执行线程的任务的)
        Runnable target = new MyRunnable();
        // 4.使用默认Thread线程类创建线程对象 并将线程任务对象作为参数传递给线程对象
        // 把线程任务对象包装成线程对象.且可以指定线程名称
        // Thread t = new Thread(target);
        Thread t = new Thread(target,"1号线程");
        // 5.调用线程对象的start()方法启动线程
        t.start();

        Thread t2 = new Thread(target);
        // 调用线程对象的start()方法启动线程
        t2.start();

        for(int i = 0 ; i < 10 ; i++ ){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
        }
    }
}

// 1.创建一个线程任务类实现Runnable接口。
class MyRunnable implements Runnable{
    // 2.重写run()方法
    @Override
    public void run() {
        for(int i = 0 ; i < 10 ; i++ ){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
            // System.out.println(getName()+"==>"+i);
            // 注意:在这里直接使用getName()就会报错,因为Runnable的实现类只是一个线程任务类,而不是线程类
        }
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标(任务)。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

优缺点

缺点:

  • 相对比继承Thread类创建线程代码复杂一点。

  • 不能直接得到线程执行的结果。

优点:

  • 线程任务类只是实现了Runnable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)

  • 同一个线程任务对象可以被包装成多个线程对象

  • 适合多个多个线程去共享同一个资源

  • 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立

  • 线程池可以放入实现Runable或Callable线程任务对象。注意:其实Thread类本身也是实现了Runnable接口的

3. Thread和Runnable的区别

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。

  2. 可以避免Java中的单继承的局限性。

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

  4. 线程池只能放入实现Runable或Callable类线程。

面试题:实现 Runnable 接口实现线程的优势?

为什么说实现 Runnable 接口比继承 Thread 类实现线程要好?好在哪里呢?

  • 首先,我们从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,也就任务与线程相分离,权责分明。Thread 类只负责线程启动和属性设置等内容,Runnable 的run方法只负责需要执行的任务。

  • 第二点就是在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字,那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。

  • 第三点好处在于避免Java 中的单继承的局限性,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。

综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。

以上就是最为常用的创建线程的两种方式,一种是继承Thread类方式,一种是实现Runnable接口方式。

4. Callable接口

除了以上两种方式呢,还可以通过实现有返回值的Callable 接口创建线程,Runnable创建线程是无返回值的,而Callable和与之相关的Future、FutureTask,它们可以把线程执行的结果作为返回值返回,如代码所示,实现了Callable接口,并且给它的泛型设置成String,然后它会返回一个字符串。

实现Callable接口步骤:

  • 定义一个线程任务类实现Callable接口 , 申明线程执行的结果类型。

  • 重写线程任务类的call方法,这个方法可以直接返回执行的结果。

  • 创建一个Callable的线程任务对象。

  • 把Callable的线程任务对象包装成一个FutureTask未来任务对象。

  • 把未来任务对象FutureTask包装成线程对象Thread

  • 调用线程的start()方法启动线程。

示例代码:

public class ThreadDemo {
    public static void main(String[] args) {
        // 3.创建一个Callable的线程任务对象
        Callable call = new MyCallable();
        // new Thread(call); 报错!!!
        
        // 4.把Callable任务对象包装成一个未来任务对象
        //      -- public FutureTask(Callable<V> callable)
        // 未来任务对象是啥,有啥用?
        //      -- 未来任务对象其实就是一个Runnable对象:这样就可以被包装成线程对象!
        //      -- 未来任务对象可以在线程执行完毕之后去得到线程执行的结果。
        FutureTask<String> task = new FutureTask<>(call);
        // 5.把未来任务对象包装成线程对象
        Thread t = new Thread(task);
        // 6.启动线程对象
        t.start();

        for(int i = 1 ; i <= 10 ; i++ ){
            System.out.println(Thread.currentThread().getName()+" => " + i);
        }

        // 在最后去获取线程执行的结果,如果线程没有结果,让出CPU等线程执行完再来取结果
        try {
            String rs = task.get(); // 获取call方法返回的结果(正常/异常结果)
            System.out.println(rs);
        }  catch (Exception e) {
            e.printStackTrace();
        }

    }
}

// 1.创建一个线程任务类实现Callable接口,申明线程返回的结果类型
class MyCallable implements Callable<String>{
    // 2.重写线程任务类的call方法!
    @Override
    public String call() throws Exception {
        // 需求:计算1-10的和返回
        int sum = 0 ;
        for(int i = 1 ; i <= 10 ; i++ ){
            System.out.println(Thread.currentThread().getName()+" => " + i);
            sum+=i;
        }
        return Thread.currentThread().getName()+"执行的结果是:"+sum;
    }
}

 

优缺点

优点:

  • 线程任务类只是实现了Callable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)

  • 同一个线程任务对象可以被包装成多个线程对象

  • 适合多个多个线程去共享同一个资源(后面内容)

  • 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。

  • 线程池可以放入实现Runable或Callable线程任务对象。(后面了解)

  • 能直接得到线程执行的结果!

缺点:编码复杂。

 

5. 线程池

概述及好处

池化技术相信大家已经屡见不鲜了,线程池、数据库连接池、Http连接池等等都是对这个思想的应用。池化技术的思想,主要是为了减少每次获取资源的消耗,提高对资源的利用率。线程池提供了一种限制和管理资源(包括执行一个任务)。

这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

类图

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

public class Executors 官方解释:

创建一个线程池,该线程池重用在共享无界队列之外运行的固定数量的线程。在任何时候,最多第n线程将是活动的处理任务。如果在所有线程都处于活动状态时提交其他任务,则它们将在队列中等待,直到线程可用。如果任何线程在关闭前的执行过程中由于失败而终止,则在需要执行后续任务时,新线程将取代它。池中的线程将一直存在,直到显式关闭。

Executor、 ExecutorService 、 Executors

这三者均是 Executor 框架中的一部分。Java 开发者很有必要学习和理解他们,以便更高效的使用 Java 提供的不同类型的线程池。总结一下这三者间的区别,以便大家更好的理解:

  • Executor 和 ExecutorService

    • ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口

    • Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的对象。

    • Executor 中的 execute() 方法不返回任何结果,而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。

    • Executor除了允许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。

  • Executors 类提供工厂方法用来创建不同类型的线程池。

    比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。

总之,需要通过Executor框架的工具类 Executors来实现创建线程池。

我们可以创建三种类型的ThreadPoolExecutor

  • CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。

    • 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。

    • 若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。

    • 所有线程在当前任务执行完毕后,将返回线程池进行复用。

  • ScheduledThreadPoolExecutor主要用来在给定的延迟后运行任务,或者定期执行任务。这个在实际项目中基本不会被用到,也不推荐使用,只需要简单了解一下它的思想即可。

  • FixedThreadPool:该方法返回一个固定线程数量的线程池。

    • 该线程池中的线程数量始终不变。

    • 当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。

    • 若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

  • SingleThreadExecutor:方法返回一个只有一个线程的线程池。

    • 当有一个新的任务提交时,任务会被保存在一个任务队列中,

    • 待线程空闲,按先入先出的顺序执行队列中的任务。

使用线程池

使用线程池创建线程对象的步骤:

  1. 创建线程池对象。

  2. 创建Runnable接口子类对象。

  3. 提交Runnable接口子类对象。

  4. 关闭线程池(一般不做)。

Runnable实现类代码:

public class ThreadPool_ {
    public static void main(String[] args) {
        // a.创建一个线程池,指定线程的固定数量是3.
        // new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
        ExecutorService pool = Executors.newFixedThreadPool(3);
        // b.创建线程的任务对象。
        Runnable target = new MyRunnable();
        // c.把线程任务放入到线程池中去执行。
        pool.submit(target);// 提交任务,此时会创建一个新线程,自动启动线程执行!
        pool.submit(target);// 提交任务,此时会创建一个新线程,自动启动线程执行!
        pool.submit(target);// 提交任务,此时会创建一个新线程,自动启动线程执行!
        pool.submit(target);// 不会再创建新线程,会复用之前的线程来处理这个任务

        pool.shutdown();// 等待任务执行完毕以后才会关闭线程池
        //pools.shutdownNow(); // 立即关闭线程池的代码,无论任务是否执行完毕!
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i  = 0 ; i < 5 ; i++ ){
            System.out.println(Thread.currentThread().getName()+" => "+i);
        }
    }
}

Callable测试代码:

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果.

  • V get() : 获取计算完成的结果。

public class ThreadPool_2 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        Future<String> submit = executorService.submit(new MyCallable1());
        Future<String> submit1 = executorService.submit(new MyCallable1());
        Future<String> submit2 = executorService.submit(new MyCallable1());
        Future<String> submit3 = executorService.submit(new MyCallable1());
        try {
            System.out.println("submit" + submit.get());
            System.out.println("submit1" + submit1.get());
            System.out.println("submit2" + submit2.get());
            System.out.println("submit3" + submit3.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        executorService.shutdown();
    }
}
class MyCallable1 implements Callable<String>{
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            sum += i;
        }
        return  "结果为" + sum;
    }
}

 

在《阿里巴巴Java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。

而且强制线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活:

  • CachedThreadPoolScheduledThreadPool : 允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程导致OOM。

  • FixedThreadPoolSingleThreadExecutor: 允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。

另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

下面我们就对ThreadPoolExecutor的使用方法进行一个详细的概述。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

构造函数的参数含义如下:

corePoolSize:指定了线程池中的核心线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;

maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;

keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;

TimeUnit:keepAliveTime的单位;

workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;

threadFactory:线程工厂,用于创建线程,一般用默认即可;

handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;

有关这些参数的详细使用看这里:

线程池的练习

需求: 使用线程池方式执行任务,返回1----n的和

分析: 因为需要返回求和结果,所以使用Callable方式的任务

代码:

public class Exec {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        Future<Integer> rs1 = executorService.submit(new SumCallable(1, 20));
        Future<Integer> rs2 = executorService.submit(new SumCallable(21, 50));
        Future<Integer> rs3 = executorService.submit(new SumCallable(51, 100));
        try {
            System.out.println(rs1.get() + rs2.get() + rs3.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        executorService.shutdown();
    }
}


class SumCallable implements Callable<Integer>{
    private final Integer L;
    private final Integer R;

    public SumCallable(Integer L, Integer R) {
        this.L = L;
        this.R = R;
    }
    
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = L; i <= R; i++) {
            sum += i;
        }
        return sum;
    }
}

面试题:线程池有哪些参数?

  • 1.corePoolSize核心线程数,线程池中始终存活的线程数。

  • 2.maximumPoolSize: 最大线程数,线程池中允许的最大线程数。

  • 3.keepAliveTime: 存活时间,线程没有任务执行时最多保持多久时间会终止。

  • 4.unit: 单位,参数keepAliveTime的时间单位,7种可选。

  • 5.workQueue: 一个阻塞队列,用来存储等待执行的任务,均为线程安全,7种可选。

  • 6.threadFactory: 线程工厂,主要用来创建线程,默及正常优先级、非守护线程。

  • 7.handler拒绝策略,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。

面试题:简单说下线程池的拒绝策略有哪些?

  • AbortPolicy:直接丢弃任务,抛出异常,这是默认策略

  • CallerRunsPolicy:只用调用者所在的线程来处理任务

  • DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务

  • DiscardPolicy:直接丢弃任务,也不抛出异常

除了以上演示的四种创建线程的方式之外,还有其他比如匿名内部类、定时器、Lambda表达式等等方式都可以去创建线程,但是需要注意的是这些方法只不过是在语法层面上的创建线程,其实严格来说,创建多线程的方式只有一种,详细内容看这里:【Java JUC】多线程的创建方式有哪些?为什么说本质上只有一种实现线程的方式? (imyjs.cn)

文章太长,影响阅读效果,后续内容请看下一篇。

微信关注

                编程那点事儿

本站为非盈利性站点,所有资源、文章等仅供学习参考,并不贩卖软件且不存在任何商业目的及用途,如果您访问和下载某文件,表示您同意只将此文件用于参考、学习而非其他用途。
本站所发布的一切软件资源、文章内容、页面内容可能整理来自于互联网,在此郑重声明本站仅限用于学习和研究目的;并告知用户不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
如果本站相关内容有侵犯到您的合法权益,请仔细阅读本站公布的投诉指引页相关内容联系我,依法依规进行处理!
作者:理想
链接:https://www.imyjs.cn/archives/1117
THE END
二维码
【Java JUC】Java多线程并发编程零基础小白入门(上)
前言 在被面试官疯狂打击后,我决定重新系统学习JUC……
<<上一篇
下一篇>>
文章目录
关闭
目 录