多线程、ThreadLocal

发布时间:2023年12月29日

1.ThreadLocal是什么?(难度:★★ 频率:★★★★)

1.作用
ThreadLocal是一种线程隔离机制, 使得每个线程都可以拥有自己独立的变量副本,从而避免了多线程环境下的线程安全问题。

public class Demo {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 
    static void print(String str){
        System.out.println(str + ":" + threadLocal.get());
    }
 
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("abc");
                print("thread1 variable");
            }
        });
 
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("def");
                print("thread2 variable");
            }
        });
 
        thread1.start();
        thread2.start();
    }
}

2.内部结构和原理
在这里插入图片描述
最初的设计
每个ThreadLocal是自己维护一个ThreadLocalMap, key是当前线程, value是要存储的局部变量, 这样就可以达到各个线程的局部变量隔离的效果

JDK8的设计
每个Thread维护一个ThreadLocalMap, 这个Map的key是ThreadLocal本身, value是要存储的变量. 具体的流程如下

这样设计的优点:

  1. 每个Map存储的Entry数量变少,之前Entry的数量是由线程个数决定的, 线程个数越多, Entry就越多, 而现在是由ThreadLocal决定的, 在实际开发中, ThreadLocal数量往往少于线程数
  2. 当Thread销毁的时候, ThreadLocalMap会随之销毁, 减少内存的使用, 早期的方案中线程执行结束并不会把ThreadLocalMap销毁(垃圾回收)

3.ThreadLocal与synchronized对比
虽然ThreadLocal与synchronized都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同

  1. 每个Thread线程内部都有一个Map(ThreadLocalMap)
  2. Map中存储了ThreadLocal对象(key)和变量副本(value)
  3. Thread内部的Map是ThreadLocal维护的, 由ThreadLocal负责向map中获取和设置线程的变量值
  4. 对于不同的线程, 每个获取副本值时, 别的线程并不能获取到当前线程的副本值, 形成了线程的隔离, 互不干扰
synchronizedThreadLocal
原理同步机制采用时间换空间的方法, 只提供了一份变量, 让不同的线程排队访问ThreadLocal采用空间换时间的方式, 为每一个线程提供了一份变量的副本, 从而实现同时访问, 互不干扰
侧重点多个线程之间访问资源的同步多线程中让每个线程之间数据相互隔离

4.源码分析

  • set方法

在这里插入图片描述

  • get方法

在这里插入图片描述
在这里插入图片描述

2.ThreadLocal导致内存泄漏(难度:★★ 频率:★★★)

1.内存泄漏和内存溢出的区别

内存溢出内存泄漏
定义内存溢出指的是程序在运行过程中申请的内存超过了系统或者进程所能提供的内存大小(结果)内存泄漏指的是程序中已经不再需要的内存未被释放,造成系统内存的浪费(起因)
原因通常是由于程序中存在大量的内存申请,而且没有及时释放,导致系统的可用内存被耗尽内存泄漏通常是由于程序中存在指针或引用,指向了不再使用的内存块,但程序却没有释放这些内存
表现当内存溢出发生时,程序通常会崩溃,并且系统可能会报告无法分配内存的错误内存泄漏不会导致程序立即崩溃,但随着时间的推移,系统可用内存会逐渐减少,最终可能导致系统变慢或者崩溃

总体来说,内存溢出是由于申请的内存过多,超出了系统限制,而内存泄漏是因为未能及时释放已经不再使用的内存。

解决内存溢出和内存泄漏的方法通常包括合理管理内存的申请和释放过程,使用合适的数据结构,以及利用内存管理工具进行检测和优化。

需要说明一点: 虽然内存泄漏可能会导致内存溢出,但内存溢出也可能是由于其他原因,例如程序中存在大量的内存申请,但这些内存并没有被泄漏,而是在程序执行期间一直保持被占用状态,最终导致系统内存耗尽。

2.强引用和弱引用的区别

  • 强引用
    最常见的引用类型, 如果一个对象具有强引用,即使系统面临内存不足的情况,垃圾回收器也不会回收具有强引用的对象
Object obj = new Object(); // 强引用
  • 弱引用
    当垃圾回收器进行扫描时,无论内存是否充足,都会回收只有弱引用的对象。
    弱引用通常用于构建缓存和实现类似的功能,使得在内存不足时,可以更容易地释放一些占用内存较大但仍可以重新计算或重新加载的对象
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用

总结:

  1. 强引用可以阻止对象被垃圾回收,只有在没有任何强引用指向对象时,垃圾回收器才会考虑回收该对象。
  2. 弱引用相对较弱,即使还有弱引用指向对象,垃圾回收器仍然可以在需要时回收该对象。
  3. 强引用适合确保对象不被提前回收的场景,而弱引用适合那些在内存紧张时可以被更容易释放的场景。

3.哪些情况下, ThreadLocal会导致内存泄漏?
3.1 长时间存活的线程

public class MyRunnable implements Runnable {
    private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        MyObject obj = new MyObject();
        myThreadLocal.set(obj);

        // 执行任务...

        // 如果线程一直存活,myThreadLocal 将一直持有对 obj 的引用,即使任务执行完毕。
    }
}

在这个例子中,即使任务执行完毕,ThreadLocal 对象仍然持有对 MyObject 的引用,而线程的生命周期可能会很长,导致 MyObject 无法被垃圾回收,从而引发内存泄漏。

为了避免这种情况,需要在不再需要 ThreadLocal 存储的对象时,显式调用 remove() 方法来清理 ThreadLocal。这样可以确保 ThreadLocal 对象中的弱引用被正确清理,从而防止内存泄漏。例如:

public class MyRunnable implements Runnable {
    private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        try {
            MyObject obj = new MyObject();
            myThreadLocal.set(obj);

            // 执行任务...

        } finally {
            // 清理 ThreadLocal,防止内存泄漏
            myThreadLocal.remove();
        }
    }
}

3.2 使用线程池
如果在使用线程池的情况下,ThreadLocal被设置在某个任务中,而这个任务在线程池中执行完成后线程被放回线程池而不是销毁,那么ThreadLocal可能在下一次任务执行时仍然持有对上次设置的对象的引用。

ExecutorService executorService = Executors.newFixedThreadPool(5);

executorService.submit(() -> {
    MyObject obj = new MyObject();
    myThreadLocal.set(obj);

    // 执行任务...

    // 线程被放回线程池,但 ThreadLocal 可能仍然持有对 obj 的引用。
});

为了避免这类问题,确保在ThreadLocal不再需要时,调用remove()方法清理它所持有的对象引用。这通常在任务执行结束时或者线程即将被销毁时执行。例如:

public class MyRunnable implements Runnable {
    private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        try {
            MyObject obj = new MyObject();
            myThreadLocal.set(obj);

            // 执行任务...

        } finally {
            // 清理 ThreadLocal,防止内存泄漏
            myThreadLocal.remove();
        }
    }
}

3.项目中哪些地方用到了多线程?(难度:★★★ 频率:★★★★★)

  • 定时任务: 定时处理数据进行统计等
  • 异步处理: 发邮件、记录日志、发短信等等, 例如注册成功后发激活邮件
  • 批量处理: 缩短响应时间

4.死锁以及死锁产生的条件?(难度:★★ 频率:★★★★★)

死锁是指多个线程(两个或以上)在执行过程中互相等待对方释放资源, 无法继续执行下去.

在一个典型的死锁情况中, 每个进程都在等待某个资源, 但同时拥有另一个资源, 由于每个进程都不愿意先释放自己已经占有的资源, 所以形成了相互等待的状况

1.死锁的4个必要条件

  1. 互斥条件: 指线程对己经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  2. 占有且等待条件: 指一个线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源
  3. 不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
  4. 循环等待条件: 进程之间形成一个循环等待链,每个进程都在等待下一个进程所占有的资源。

5.i++是否线程安全?(难度:★ 频率:★)

i++操作本身并不是线程安全的

i++实际上是一个复合操作,包括读取 i 的当前值、将其加一、然后将结果写回 i。其中涉及到读取、修改、写入三个步骤。如果两个线程同时执行这段代码,可能会导致竞态条件,使得最终的结果不是期望的增加2,而可能是增加1或者其他值。

解决方式一: 使用同步(synchronization)

synchronized (lock) {
    i++;
}

解决方式二: 使用原子类(Atomic Classes)

AtomicInteger i = new AtomicInteger(0);
i.incrementAndGet();

6.synchronized的使用(难度:★★ 频率:★★)

1.synchronized的三种用法

  1. 修饰普通方法
public synchronized void increase() {
   
}
  1. 修饰静态方法
public static synchronized void increase() {
   
}
  1. 修饰代码块
public Object synMethod(Object a1) {
    synchronized(a1) {
        // 操作
    }
}

2.synchronized用于静态方法与普通方法有区别吗?

  1. 普通方法的synchronized
public class MyClass {
    public synchronized void instanceMethod() {
        // 实例方法的同步代码块
    }
}

实例方法中, 锁住的是当前实例对象(this), 对于MyClass类的不同实例, 它们的实力方法是独立的, 可以同时执行

  1. 静态方法的synchronized
public class MyClass {
    public static synchronized void staticMethod() {
        // 静态方法的同步代码块
    }
}

静态方法中, 锁住的是整个类的Class对象, 对于MyClass类的所有实例,同一时间只能有一个线程执行该静态方法。

7.SimpleDateFormat线程安全吗?怎么保证线程安全?(难度:★★★ 频率:★★★)

SimpleDateFormat类在多线程环境下是线程不安全的。

SimpleDateFormat内部维护了一个Calendar实例,而Calendar是线程不安全的

Calendar 的实现并没有在设计上考虑到多线程并发访问的情况。因此,多个线程可能同时修改 Calendar 内部的状态,而不受到足够的同步或锁的保护,从而导致线程安全问题。

解决方式一: 使用局部变量
在每个线程中创建一个独立的 SimpleDateFormat 实例,而不是共享一个实例。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

解决方式二: 使用线程安全的替代方案
如果你需要在多线程环境中进行日期格式化和解析操作,可以考虑使用 java.time.format.DateTimeFormatter,它是 java.time 包中的类,设计为线程安全的。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

8.线程池的优缺点难度(难度:★★ 频率:★★)

1.优点
线程池通过提供一种有效的线程管理和调度机制,帮助提高应用程序的性能和可维护性,尤其在处理大量并发任务时,线程池是一种强大而有效的工具。

  1. 线程管理:
    线程池可以有效管理系统资源, 避免频繁创建和销毁线程, 减少系统开销, 通过重复利用线程, 降低资源的占用
  2. 调度和控制
    线程池允许对线程的数量进行有效的控制,可以防止系统因过度并发而陷入性能下降的状态。通过配置线程池的参数,可以灵活地调整线程的数量和行为。

2.缺点

  1. 资源占用: 线程池本身会占用一定的系统资源,包括内存和 CPU 资源。如果线程池设置不当,可能会导致资源浪费。
  2. 任务队列阻塞: 线程池的任务队列如果满了,新的任务可能会被阻塞或者拒绝。这可能导致任务延迟执行,特别是在高负载情况下。
  3. 难以调试: 当线程池中的线程发生问题时,调试可能会变得复杂。由于线程的生命周期和执行过程由线程池管理,追踪问题可能会比直接管理线程的情况更加困难。
  4. 配置复杂: 需要合理配置线程池的参数,包括线程数量、任务队列大小等。配置不当可能导致性能问题,需要开发人员具有一定的经验和调优技能。
  5. 不适用于所有场景: 线程池并不是在所有情况下都是最佳选择。对于某些类型的任务,例如计算密集型任务,其他并发模型可能更为合适。

9.线程池有哪些参数?(难度:★★ 频率:★★★★★)

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize 核心线程数
    核心线程数是指线程池中保持存活的最小线程数量, 这些线程会一直存活, 即使它们处于空闲状态
  • maximumPoolSize 最大线程数
    最大线程数是指线程池中允许的最大线程数量。当线程池中的任务队列已满,且活动线程数未达到最大线程数时,线程池会创建新的线程来执行任务,直到达到最大线程数。超过最大线程数的任务会根据线程池的拒绝策略进行处理,可能会被拒绝执行或者以其他方式处理。
  • keepAliveTime 非核心线程存活时间
  • unit 指定keepAliveTime的单位
  • workQueue 阻塞队列
    它充当了缓冲区的角色, 任务在没有核心线程处理是, 优先将任务扔到阻塞队列中
  • threadFactory 线程工厂
    指定创建线程的方式, 例如设置线程的名称、优先级、是否为守护线程等待
  • handler 拒绝政策
    用于定义当前线程池无法接受新任务时的策略

当我们核心线程数已经到达最大值、阻塞队列也已经放满了所有的任务、而且我们工作线程个数已经达到最大线程数, 此时如果还有新任务, 就只能走拒绝策略了

10.阻塞队列的作用以及用法(难度:★★ 频率:★★★)

1.作用

  1. 任务缓冲
    作为一个缓冲区, 可以在生产者产生任务时缓存这些任务, 等待线程池中的线程来执行
  2. 任务调度
    不同类型的BlockingQueue实现了不同的任务调度策略, 例如FIFO(先进先出)、LIFO(后进先出)、优先级队列等, 这有助于更灵活的控制任务的执行顺序

2.用法
在线程池中, BlockingQueue主要通过以下两个参数进行配置

  • corePoolSize和maximumPoolSize这个两个参数来指定线程池的基本大小和最大大小
    • 当线程池中的线程未达到corePoolSize时, 新任务将创建新线程.
    • 当线程池中的线程数达到corePoolSize且任务队列未满时,新任务将被放入队列等待
    • 当队列也满了,且线程池中的线程数未达到 maximumPoolSize 时,新任务将创建新线程
    • 当线程池中的线程数达到 maximumPoolSize 时,新任务将由饱和策略处理
  • workQueue: 这个参数就是 BlockingQueue,用于存储等待执行的任务。通过选择不同的 BlockingQueue 实现,可以实现不同的任务调度策略。
import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个LinkedBlockingQueue作为任务队列
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(10);

        // 创建一个ThreadPoolExecutor,使用LinkedBlockingQueue作为任务队列
        ExecutorService executor = new ThreadPoolExecutor(
                5,  // corePoolSize
                10, // maximumPoolSize
                1,  // keepAliveTime
                TimeUnit.SECONDS,
                queue);

        // 提交任务到线程池
        for (int i = 0; i < 15; i++) {
            executor.submit(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println("Task completed by: " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

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

打印结果:
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-3
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-3
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-3

在这个例子中,LinkedBlockingQueue 作为任务队列,可以存储最多 10 个等待执行的任务。线程池的核心线程数为 5,最大线程数为 10,因此在任务队列未满时,新任务将放入队列等待。如果队列已满,新任务将创建新线程执行,但不会超过最大线程数。

因为开启了15个线程, 而核心线程数+阻塞队列容量正好为15个, 所以不会创建新的线程

11.不同阻塞队列的区别(难度:★★ 频率:★★★)

1.ArrayBlockingQueue
基于数组实现的有界队列
固定容量,一旦创建就不能更改。
需要指定容量,适用于任务数量固定的情况。

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

使用 ArrayBlockingQueue 有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到 corePoolSize 时,则会将新的任务加入到等待队列中。若等待队列已满,即超过 ArrayBlockingQueue 初始化的容量,则继续创建线程,直到线程数量达到 maximumPoolSize 设置的最大线程数量,若大于 maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在 corePoolSize 以下,反之当任务队列已满时,则会以 maximumPoolSize 为最大线程数上限

2.LinkedBlockingQueue
基于链表实现的有界或无界队列
可以选择是否指定容量,如果不指定容量则默认是 Integer.MAX_VALUE
适用于任务数量不固定的情况

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你 corePoolSize 设置的数量,也就是说在这种情况下 maximumPoolSize 这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到 corePoolSize 后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。

3.SynchronousQueue
一个不存储元素的队列
每个插入操作必须等待另一个线程的对应移除操作,反之亦然。
主要用于直接传递任务的场景,一个线程产生任务,另一个线程消费任务

4.PriorityBlockingQueue
基于优先级堆的无界队列。
元素需要实现Comparable接口或者在构造方法中提供Comparator

5.DelayedWorkQueue
一个支持延时获取元素的无界队列,用于实现定时任务。
元素需要实现 Delayed 接口

12.线程池有哪几种?它们分别对应什么队列?(难度:★★ 频率:★★★)

FixedThreadPoolSingleThreadExecutorScheduledThreadPoolCachedThreadPool
名称固定大小线程池单线程线程池定时任务线程池缓存线程池
特点固定线程数量的线程池,适用于负载较重的服务器只有一个工作线程的线程池,确保所有任务按顺序执行支持定时及周期性任务执行的线程池线程数量根据需求动态调整,线程空闲一定时间后被回收

1.FixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
  • corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池,是其优势;
  • keepAliveTime = 0 该参数默认对核心线程无效,而 FixedThreadPool 全部为核心线程;
  • workQueue 为 LinkedBlockingQueue(无界阻塞队列),队列最大值为 Integer.MAX_VALUE。
    如果任务提交速度持续大于任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢出。是其劣势;
  • FixedThreadPool 的任务执行是无序的;
public class NewFixedThreadPoolTest {

    public static void main(String[] args) {
        System.out.println("主线程启动");
        // 1.创建1个有2个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(2);

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
            }
        };
        // 2.线程池执行任务(添加4个任务,每次执行2个任务,得执行两次)
        threadPool.submit(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        System.out.println("主线程结束");
    }
}

上述代码:创建了一个有2个线程的线程池,但一次给它分配了4个任务,每次只能执行2个任务,所以,得执行两次。

该线程池重用固定数量的线程在共享的无界队列中运行。 在任何时候,最多 nThreads 线程将是活动的处理任务。如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待,直到有线程可用。 所以,它会一次执行 2 个任务(2 个活跃的线程),另外 2 个任务在工作队列中等待着。

submit() 方法和 execute() 方法都是执行任务的方法。它们的区别是:submit() 方法有返回值,而 execute() 方法没有返回值。

2.CachedThreadPool
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
  • corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制;
  • keepAliveTime = 60s,线程空闲 60s 后自动结束
  • workQueue 为 SynchronousQueue 同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为 CachedThreadPool 线程创建无限制,不会有队列等待,所以使用 SynchronousQueue

适用场景:快速处理大量耗时较短的任务,如 Netty 的 NIO 接受请求时,可使用 CachedThreadPool。

public class NewCachedThreadPool {

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            threadPool.execute(() -> {
                System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

3.SingleThreadExecutor
创建单个线程数的线程池,它可以保证先进先出的执行顺序。

4.SingleThreadScheduledExecutor
创建一个单线程的可以执行延迟任务的线程池。

public class SingleThreadScheduledExecutorTest {

    public static void main(String[] args) {
        ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
        System.out.println("添加任务,时间:" + new Date());
        threadPool.schedule(() -> {
            System.out.println("任务被执行,时间:" + new Date());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        }, 2, TimeUnit.SECONDS);
    }
}

13.线程池执行流程(难度:★★ 频率:★★★★★)

在这里插入图片描述

14.线程工厂的作用, 以及使用方法(难度:★ 频率:★★)

ThreadFactory是一个接口, 用于创建新线程的工厂, 它允许你自定义线程的创建过程, 例如设置线程的名称、优先级、守护状态等…

import java.util.concurrent.*;

public class CustomThreadFactoryExample {
    public static void main(String[] args) {
        // 创建一个自定义的ThreadFactory
        ThreadFactory customThreadFactory = new CustomThreadFactory("CustomThread");

        // 使用自定义的ThreadFactory创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5, customThreadFactory);

        // 提交一些任务
        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> System.out.println(Thread.currentThread().getName()));
        }

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

// 自定义的ThreadFactory实现
class CustomThreadFactory implements ThreadFactory {
    private final String threadNamePrefix;

    public CustomThreadFactory(String threadNamePrefix) {
        this.threadNamePrefix = threadNamePrefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        // 创建新线程并设置线程名称
        Thread thread = new Thread(r, threadNamePrefix + "-" + System.nanoTime());
        // 设置为后台线程(可选)
        thread.setDaemon(false);
        // 设置线程优先级(可选)
        thread.setPriority(Thread.NORM_PRIORITY);
        return thread;
    }
}

15.线程池拒绝策略有哪些?默认是哪个?(难度:★ 频率:★★★★)

一、AbortPolicy
这是默认的拒绝策略,当队列满时直接抛出
RejectedExecutionException异常,阻止系统继续运行。

ExecutorService executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    timeUnit,
    new LinkedBlockingQueue<>(capacity),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy());

二、CallerRunsPolicy
新任务会被直接在提交任务的线程中运行。这样做可以避免任务被拒绝,但会影响任务提交的线程的性能。

ExecutorService executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    timeUnit,
    new LinkedBlockingQueue<>(capacity),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy());

三、DiscardPolicy
新任务被直接丢弃,不做任何处理。

ExecutorService executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    timeUnit,
    new LinkedBlockingQueue<>(capacity),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.DiscardPolicy());

四、DiscardOldestPolicy
尝试将最旧的未处理任务从队列中删除,然后重新尝试执行任务

ExecutorService executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    timeUnit,
    new LinkedBlockingQueue<>(capacity),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.DiscardOldestPolicy());

16.如何捕获线程池中的异常? (难度:★★ 频率:★★★★★)

验证主线程无法捕获线程池中异常

public static void main(String[] args) {
    try {
        System.out.println("主线程开始");
        // 创建线程池
        ExecutorService executor = Executors.newSingleThreadExecutor();
        // 执行线程方法
        executor.execute(()->{
            System.out.println("子线程运行开始");
            int i = 1 / 0;
            System.out.println("子线程运行结束");
        });
        executor.shutdown();
        System.out.println("主线程结束");
    } catch (Exception e) {
        System.out.println("异常信息:" + e.getMessage());
    }
}

在这里插入图片描述
在上面的代码中, 异常无法被捕获的原因是因为异常发生在子线程中,而主线程并不直接捕获这个异常。

execute方法提交任务后,异常会被线程池中的线程捕获并处理,但是这个异常处理是在线程内部进行的,不会传递到主线程中。

我们发现不论是使用execute方法或者submit方法提交任务, 都没有办法在主线程中捕获到异常, 有没有解决方式?

  • 方式一: 使用submit提交任务, 并使用阻塞方法(get)直达任务完成, 如果这个过程中发生了异常, 会在主线程中被捕获
  • 方式二: 自定义ThreadPoolExecutor作为线程池

1.使用submit提交任务, 再通过阻塞方法等待任务完成, 如果这个过程中发生了异常, 会在主线程中被捕获
你可以通过Future对象的get方法来获取异常,但需要注意的是,如果任务执行过程中发生了异常,调用get方法会抛出ExecutionException,你需要在主线程中处理这个异常。

try {
    System.out.println("主线程开始");
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<?> future = executor.submit(() -> {
        System.out.println("运行开始");
        int i = 1 / 0;
        System.out.println("运行结束");
    });
    executor.shutdown();

    // 获取任务执行的结果,这里会阻塞直到任务完成
    future.get();
    System.out.println("主线程结束");
} catch (Exception e) {
    System.out.println("捕获到异常:" + e.getMessage());
}

上述的方法使用submit提交任务, 那如果使用execute提交任务, 可以捕获线程池中的异常吗?

submit和execute方法的区别
submit和execute都可以提交任务, 两者有一些关键的区别

  • 返回值
    • execute 方法没有返回值,它用于执行实现了Runnable接口的任务。
    • submit 方法返回一个Future对象,该对象可以用于获取任务执行的结果。这是因为submit可以执行实现了Callable接口的任务,它们可以返回一个结果。
  • 任务类型
    • execute 方法接受Runnable类型的任务,这种任务没有返回值。
    • submit 方法既可以接受Runnable类型的任务,也可以接受Callable类型的任务,后者可以返回结果。
  • 异常处理
    • execute方法无法处理任务执行过程中抛出的异常,异常会被直接抛到调用者
    • submit方法可以通过Future对象的get方法来获取任务执行过程中抛出的异

Callable的返回值类型是在实现接口时指定的, 例如下面这个例子

public class GetStrService implements Callable<String> {
    private int i;

    public GetStrService(int i) {
        this.i = i;
    }

    @Override
    public String call() throws Exception {
        int t=(int) (Math.random()*(10-1)+1);
        System.out.println("第"+i+"个任务开始啦:"+Thread.currentThread().getName()+"准备延时"+t+"秒");
        Thread.sleep(t*1000);
        return "第"+i+"个GetStrService任务使用的线程:"+Thread.currentThread().getName();
    }
}

2.自定义ThreadPoolExecutor作为线程池

// 自定义线程池
class MyThreadPoolExecutor extends ThreadPoolExecutor {
    public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        System.out.println("捕获到异常。异常信息为:" + t.getMessage());
        System.out.println("异常栈信息为:");
        t.printStackTrace();
    }
}

public static void main(String[] args) {
    System.out.println("主线程开始");
    // 创建线程池
    ExecutorService executor = new MyThreadPoolExecutor(5,50, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(20));
    // 执行线程方法
    executor.execute(()->{
        System.out.println("子线程运行开始");
        int i = 1 / 0;
        System.out.println("子线程运行结束");
    });
    executor.shutdown();
    System.out.println("主线程结束");
}

在这里插入图片描述

文章来源:https://blog.csdn.net/qq_24099547/article/details/135289051
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。