侧边栏壁纸
博主头像
太上问情 博主等级

人非太上,岂能忘情。

  • 累计撰写 12 篇文章
  • 累计创建 9 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

多线程

太上问情
2024-11-26 / 0 评论 / 0 点赞 / 5 阅读 / 0 字 / 正在检测是否收录...

Java多线程机制

线程的定义

进程

进程就是指一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间。一个进程可以有多个线程,如在Windows操作系统中,一个运行的xx.exe就是一个进程。

线程

线程是操作系统能够进行运算调度的最小单位。线程被包含在进程中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流。在一个进程中可以并发处理多个线程,每个线程可以并行执行不同的任务。

线程是独立调度和分派的基本单位。

同一进程中的多个线程将共享该进程的全部系统资源,如虚拟地址空间、文件描述符和信号处理等。

线程的创建

Java中创建线程有3种方式。

(1)通过定义Thread类的子类创建线程。

(2)通过定义Runnable接口的实现类创建线程。

(3)通过Callable接口和Future接口创建线程。

Java主线程

当启动Java程序时,一个线程立刻运行,通常将该线程叫作程序的主线程(Main Thread)。主线程的重要性体现在以下两方面。

(1)主线程是产生其他子线程的线程。

(2)通常主线程必须最后完成执行,因为由它执行各种关闭动作。

创建线程——继承Thread类

使用Thread类实现线程的步骤如下。

(1)定义Thread类的子类并重写该类的run()方法,该方法的方法体代表该线程需要完成的任务。

(2)创建Thread类的实例,即创建线程对象。

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

public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
        System.out.println("创建的线程名称:" + this.getName());
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("运行线程:" + this.getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        // 继承Thread类,重写run方法,创建线程
        Thread t1 = new MyThread("线程1");
        t1.start();
        Thread t2 = new MyThread("线程2");
        t2.start();
    }
}

创建线程——实现Runnable接口

使用Runnable接口创建线程的步骤如下。

(1)定义Runnable接口的实现类,并且重写它的run()方法,这个方法同样是该线程的执行体。

(2)创建Runnable实现类的实例,并且将此实例作为Thread类的target创建一个Thread对象,该对象才是真正的线程对象。

(3)调用start()方法启动该线程。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 获取当前正在执行的线程对象的名称
        String threadName = Thread.currentThread().getName();
        System.out.println("运行:" + threadName);

        try {
            for (int i = 0; i < 4; i++) {
                System.out.println("线程:" + threadName + ":" + i);
                Thread.sleep(50);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程" + threadName + "运行结束");
    }


    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        t1.start();
        t2.start();
    }
}

创建线程——Callable接口和Future接口

Callable是JDK 1.5推出的接口,与Runnable接口相似,其中只有一个方法call()。call()方法可以抛出Exception类及其子类的异常,并且可以返回指定的泛型类对象。

FutureTask类代表一个可取消的异步计算任务。FutureTask是Future接口的基础实现类,包含启动异步和取消计算的方法、查看计算任务是否完成的方法和查询计算结果的方法。只有在计算完成后才能检索结果,如果计算尚未完成,那么get()方法将被阻塞。一旦计算完成,就不能重新开始或取消计算(除非使用runAndReset调用计算)。

通过Callable接口和Future接口创建线程的步骤如下。

(1)创建Callable接口的实现类,同时实现call()方法,该方法将作为线程执行体,并且有返回值。

(2)创建Callable接口的实现类的实例,使用FutureTask类来包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法获得子线程执行结束后的返回值。

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int i = 0;
        while (i < 3) {
            System.out.println(Thread.currentThread().getName() + " " + i++);
        }
        return i;
    }

    public static void main(String[] args) {
        MyCallable callable = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(callable);
        new Thread(task, "有返回值的线程").start();

        try {
            System.out.println("子线程的返回值:" + task.get());
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }

    }
}

创建线程的3种方式的对比

当采用继承Thread类的方式创建线程类时,受Java单重继承机制的影响,该线程类无法继承其他类,可能对该线程类的扩展性造成影响。

当使用Runnable接口和Callable接口创建线程类时,线程类不仅实现了Runnable接口和Callable接口,还可以继承其他类,扩展性更好。

在大多数情况下,如果只打算覆盖run()方法而不打算覆盖Thread类其他的方法,那么应该使用Runnable接口。 这很重要,因为除非开发者打算修改或增强类的基本行为,否则不应将类子类化

线程的状态控制

线程的整个生命周期可以分为5个状态

1.新建状态

使用关键字new和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。该线程对象会保持这个状态直到程序调用了它的start()方法。

2.就绪状态

当线程对象调用start()方法之后,该线程对象就进入就绪状态。就绪状态的线程处于就绪队列中,需要等待Java虚拟机中线程调度器的调度。

3.运行状态

如果就绪状态的线程获取了CPU资源,就可以执行run()方法,此时线程便处于运行状态。处于运行状态的线程最复杂,因为它可以变为阻塞状态、就绪状态和终止状态。

4.阻塞状态

如果一个线程执行了sleep()(睡眠)和suspend()(挂起)等方法,失去所占用的资源之后,那么该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。阻塞状态可以分为3种。

  • 等待阻塞:运行状态中的线程执行wait()方法,可以使线程进入等待阻塞状态
  • 同步阻塞:线程在获取synchronized同步锁时失败(因为同步锁被其他线程占用)。
  • 其他阻塞:当调用线程的sleep()方法或join()方法发出输入/输出请求时,线程就会进入阻塞状态。当sleep()方法超时,join()方法等待线程终止或超时,或者输入/输出处理完毕时,线程重新转入就绪状态。

5.终止状态

当一个处于运行状态的线程完成任务或其他终止条件发生时,该线程就切换为终止状态。

线程的状态转换

在生命周期的给定时间点上,一个线程只能处于其中一种状态。在线程的整个生命周期中,线程会在5种状态之间切换。

1730075238053

线程的优先级

  • 每个Java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
  • Java线程的优先级是一个整数,其取值范围是1(Thread.MIN_PRIORITY)~10(Thread.MAX_PRIORITY)。
  • 默认情况下,为每个线程分配一个优先级NORM_PRIORITY(5)
  • 设置线程的优先级应该使用setPriority()方法,该方法也是Thread类的成员。
  • 开发者可以通过调用Thread类的getPriority()方法来获得当前优先级的设置。
  • 具有较高优先级的线程对程序更重要,并且应该在较低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,并且非常依赖底层平台。

后台线程

Java虚拟机中一般包括两种线程,分别是用户线程和后台线程。

所谓的后台线程指的是程序运行时在后台提供的一种通用服务的线程,也就是为其他线程提供服务的线程,也称守护线程。

后台线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,也就是用户线程都结束时,程序也就终止了,同时会杀死进程中所有的后台线程。

将一个用户线程设置为后台线程的方式是在线程对象创建之前使用线程对象的setDaemon()方法。通过setDaemon(true)来设置线程为后台线程。setDaemon()方法必须在start()方法之前设定,否则会抛出IllegalThreadStateException异常。可以使用isDaemon()方法判断一个线程是前台线程还是后台线程。

线程的同步和互斥

线程安全

一个共享数据是可以由多个线程一起操作的,但是当一个共享数据在被一个线程操作的过程中,操作并未执行完毕,如果此时另一个线程也参与操作该共享数据,就会导致共享数据存在安全问题。

线程互斥

在Java中,可以通过线程互斥来解决线程安全问题。 线程互斥是指不同线程通过竞争进入临界区(共享的数据和硬件资源),为了防止发生访问冲突,在有限的时间内只允许其中之一独自使用共享资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

Java中有两种实现线程互斥的方法:一是通过同步(Synchronized)代码块同步方法实现,二是通过锁(Lock)实现。

1.同步代码块(同步监听器)

同步代码块是使用关键字synchronized修饰的语句块。使用关键字synchronized修饰的语句块会自动被加上内置锁,从而实现同步。

同步代码块的语法格式如下:

1730075654249

利用关键字synchronized就可以为代码加上锁,在该关键字后面的圆括号中所放入的就是对象锁。对象锁可以是一个任意的类的对象,但是所有线程必须共用一个锁。

关键字synchronized使用的锁保存在Java的对象头中。

2.同步方法

同步方法将子线程要允许的代码放到一个方法中,在该方法的名称前面加上关键字synchronized即可,这里默认的锁为this,即当前对象。在使用时,需要确认多线程访问的是同一个实例的同步方法才能实现同步效果。当使用关键字synchronized修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

同步方法的语法格式如下:

1730075654249-1732298261854-99

或者:

1730075865162-1732298261854-101

**注:关键字synchronized也可以修饰静态方法,此时如果调用该静态方法,就会锁住当前类的Class对象。

线程同步

广义的线程同步被定义为一种机制,用于确保两个或多个并发的线程不会同时进入临界区(共享的数据和硬件资源)。从该定义来看,线程同步和线程互斥是相同的。狭义的线程同步在线程互斥的基础上增加了对多个线程执行顺序的要求,即两个或多个并发的线程应按照特定的顺序进入临界区。可以简单地总结为,狭义的线程同步是一种强调执行顺序的线程互斥。

线程通信

线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码独立运行。在现实应用中,有些场景可能需要多个线程按照指定的规则共同完成一项任务。这时就需要多个线程互相协调,这个过程被称为线程通信。

1.线程通信的方式

线程通信主要分为3种方式,分别为共享内存、消息传递和管道流。

1)共享内存

一个进程下的多个线程共享该进程被分配的内存空间,内存空间中一些特定区域(主内存)的数据可以被该进程下的多个线程共同访问,这些数据被称为进程(程序)的公共状态。一个进程下的多个线程之间可以通过读/写内存中的公共状态来实现隐式通信。

基于共享内存的线程通信可能存在并发问题。这是因为Java内存模型规定,线程对公共状态的操作(读取、赋值等)必须在自己的栈空间中进行。因此,线程需要先从主内存中将公共状态的值复制到自己的栈空间中。后续如果读取,那么使用栈空间中的数据;后续如果修改,那么先修改自己的栈空间中副本的值,再将修改后的值写到主内存中。

线程访问共享数据的示意图:

1730076116427

volatile

关键字volatile可以修饰字段(成员变量),即规定线程对该变量的访问均需要从共享内存中获取,对该变量的修改也必须同步刷新到共享内存中,以保证资源的可见性。

过多地使用关键字volatile可能会降低程序的效率。

2)消息传递

在线程通信的消息传递过程中,最常用的就是等待通知(wait/notify)方式。等待通知方式就是将处于等待状态的线程由其他线程发出通知后重新获取CPU资源,继续执行之前没有执行完的任务。

Java提供了如下3个方法来实现线程之间的消息传递。

  • wait():导致当前线程等待,释放同步监听器的锁定;直到其他线程调用该线程的同步监听器的notify()方法或notifyAll()方法来唤醒该线程。
  • notify():随机唤醒在此同步监听器上等待的其他单个线程(再次调用wait()方法,让当前线程等待,释放同步监听器,这样才可以执行被唤醒的线程)。
  • notifyAll():唤醒所有在此同步监听器上等待的线程(再次调用wait()方法,让当前线程等待,释放同步监听器,这样才可以执行被唤醒的线程)。

上述3个方法的调用者必须是同步代码块或同步方法中的同步监听器,否则会出现IllegalMonitorStateException异常。

3)管道流

管道流是一种较少使用的线程间通信方式。和普通文件输入/输出流或网络输出/输出流不同,管道输入/输出流主要用于线程之间的数据传输,传输媒介为管道。

管道输入/输出流主要包括4种具体的实现,分别为PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,后两种面向字符。

Java的管道的输入和输出实际上是使用循环缓存数组来实现的,默认为1024,输入流从这个数组中读取数据,输出流从这个数组中写入数据。当数组已满时,输出流所在的线程就会被阻塞;当这个数组为空时,输入流所在的线程就会被阻塞。

sleep()方法和wait()方法的异同

sleep()方法和wait()方法的相同点:一旦执行方法,就可以使当前线程进入阻塞状态。

sleep()方法和wait()方法的不同点如下。

(1)声明两个方法的位置不同:Thread类中声明的是sleep()方法,Object类中声明的是wait()方法。

(2)调用的要求不同:sleep()方法可以在任何需要的场景下调用,wait()方法必须在同步代码块或同步方法中调用。

(3)是否释放同步监听器:如果两个方法都在同步代码块或同步方法中调用,那么sleep()方法不释放同步监听器,wait()方法会释放同步监听器。

线程死锁

1.什么是线程死锁

所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),如果无外力作用,那么这些进程都将无法向前推进。 线程死锁的示意图如图 :

1730076477476

2.产生死锁的原因

死锁主要是由以下4个因素造成的。

(1)互斥条件:是指线程对已经获取到的资源进行排他性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,那么请求者只能等待,直到占用资源的线程释放该资源。

(2)不可被剥夺条件:是指线程获取到的资源在自己使用完之前不能被其他线程占用,只有在自己使用完毕才由自己释放该资源。

(3)请求并持有条件:是指一个线程已经占用了至少一个资源,但又提出了新的资源请求,而新的资源已被其他线程占用,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。

(4)环路等待条件:是指在发生死锁时,必然存在一个(线程—资源)环形链,即线程集合{T0,T1,T2,…,Tn}中的T0正在等待T1占用的资源,T1正在等待T2占用的资源,依次类推,Tn正在等待T0占用的资源。 环路等待的示意图如图12.25所示。

1730076513855

当上述4个条件都成立时,便形成死锁。当然,在发生死锁的情况下,如果打破上述任何一个条件,就可以让死锁消失。

死锁Demo:

class DeadDemo implements Runnable {
    public int flag = 1;
    // 静态对象有类所有对象共享
    private static Object o1 = new Object();
    private static Object o2 = new Object();

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.printf("%s:flag = %d%n", threadName, flag);

        switch (flag) {
            case 1:
                synchronized (o1) {
                    System.out.printf("%s:取得o1锁%n", threadName);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.printf("%s:申请o2锁%n", threadName);
                    synchronized (o2) {
                        System.out.println(1);
                    }
                }
                break;
            case 0:
                synchronized (o2) {
                    System.out.printf("%s:取得o2锁%n", threadName);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.printf("%s:申请o1锁%n", threadName);
                    synchronized (o1) {
                        System.out.println(0);
                    }
                }
                break;
            default:
                break;
        }
    }

    public static void main(String[] args) {
        DeadDemo td1 = new DeadDemo();
        DeadDemo td2 = new DeadDemo();
        td1.flag = 1;
        td2.flag = 0;
        new Thread(td1, "td1").start();
        new Thread(td2, "td2").start();
    }
}

1730077478385

程序分析:当DeadDemo类的对象(td1)flag=1时,先锁定o1锁,睡眠500毫秒。而td1在睡眠的时候另一个对象(td2)flag=0的线程启动,先锁定o2锁,睡眠500毫秒。td1睡眠结束后需要锁定o2锁才能继续执行,而此时o2锁已被td2锁定;td2睡眠结束后需要锁定o1锁才能继续执行,而此时o1锁已被td1锁定;td1和td2相互等待,都需要得到对方锁定的资源才能继续执行,从而造成死锁。

3.解决死锁的方法

要想解决死锁,只需要破坏至少一个产生死锁的必要条件即可。在操作系统中,互斥条件和不可剥夺条件是操作系统规定的,没有办法人为更改,并且这两个条件明显是一个标准的程序应该具备的特性。所以,目前只有请求并持有条件和环路等待条件是可以被破坏的

产生死锁的原因其实和申请资源的顺序有很大的关系。使用资源申请的有序性原则就可以避免产生死锁。因此,解决死锁的方法有两种

(1)设置加锁顺序(线程按照一定的顺序加锁)

(2)设置加锁时限(线程获取锁的时候加上一定的时限,若超过时限则放弃对该锁的请求,并释放自己占用的锁)

线程池

熟悉 Java 多线程编程的同学都知道,当我们线程创建过多时,容易引发内存溢出,因此我们就有必要使用线程池的技术了。

1 线程池的优势

总体来说,线程池有如下的优势:

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

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

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

2 线程池的使用

线程池的真正实现类是 ThreadPoolExecutor,其构造方法有如下4种:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

可以看到,其需要如下几个参数:

  1. corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  2. maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。

  3. keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将

  4. allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  5. unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、

  6. TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

  7. workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。

  8. threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。

  9. handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

    线程池的使用流程如下:

// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

3 线程池的工作原理

下面来描述一下线程池工作的原理,同时对上面的参数有一个更深的了解。其工作原理流程图如下:

1730083343237

通过上图,相信大家已经对所有参数有个了解了。下面再对任务队列、线程工厂和拒绝策略做更多的说明。

4 线程池的参数

4.1 任务队列(workQueue)

任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:

  1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  2. LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
  3. PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
  4. DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  5. SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
  6. LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
  7. LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。

注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义

4.2 线程工厂(threadFactory)

线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:

/**
 * The default thread factory.
 */
private static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

4.3 拒绝策略(handler

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了

4 种拒绝策略:

  1. AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
  2. CallerRunsPolicy:由调用线程处理该任务。
  3. DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  4. DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

5 功能线程池

嫌上面使用线程池的方法太麻烦?其实Executors已经为我们封装好了 4 种常见的功能线程池,如下:

  • 定长线程池(FixedThreadPool)

  • 定时线程池(ScheduledThreadPool )

  • 可缓存线程池(CachedThreadPool)

  • 单线程化线程池(SingleThreadExecutor)

5.1 定长线程池(FixedThreadPool)

创建方法的源码:

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

特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。

应用场景:控制线程最大并发数。
使用示例:

// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);

5.2 定时线程池(ScheduledThreadPool )

创建方法的源码:

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}

特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。

应用场景:执行定时或周期性的任务。

使用示例:

// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务

5.3 可缓存线程池(CachedThreadPool)

创建方法的源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。

应用场景:执行大量、耗时少的任务。

使用示例:

// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);

5.4 单线程化线程池(SingleThreadExecutor)

创建方法的源码:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。

应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。

使用示例:

// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);

5.5 对比

1730084101434

6 总结

Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

其实 Executors 的 4 个功能线程池有如下弊端:

  1. FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
  2. CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
0

评论区