Java面试篇(线程池相关专题)

news2024/11/13 11:00:21

文章目录

  • 1. 为什么要使用线程池
  • 2. 线程池的核心参数和线程池的执行原理
    • 2.1 线程池的核心参数
    • 2.2 线程池的执行原理
  • 3. 线程池中常见的阻塞队列
    • 3.1 常见的阻塞队列
    • 3.2 ArrayBlockingQueue 和 LinkedBlockingQueue 的区别
  • 4. 如何确定线程池的核心线程数
    • 4.1 应用程序中任务的类型
      • 4.1.1 IO 密集型任务
      • 4.1.2 CPU 密集型任务
    • 4.2 如何确定核心线程数
  • 5. 线程池的种类
    • 5.1 固定线程数的线程池
    • 5.2 单线程线程池
    • 5.3 可缓存线程池
    • 5.4 提供延迟功能和周期执行功能的线程池
  • 6. 为什么不建议使用 Executors 类提供的静态方法创建线程池
  • 7. 线程池(多线程)的使用场景
    • 7.1 CountDownLatch
    • 7.2 多线程使用场景一:ElasticSearch 批量导入数据
    • 7.3 多线程使用场景二:数据汇总
    • 7.4 多线程使用场景三:异步调用
  • 8. 控制某个方法允许线程并发访问的线程数量(Semaphore)
  • 9. ThreadLocal
    • 9.1 ThreadLocal的基本使用
    • 9.2 ThreadLocal的实现原理&源码分析
      • 9.2.1 set 方法
      • 9.2.2 get 方法
      • 9.2.3 remove 方法
    • 9.3 ThreadLocal 的内存泄漏问题

1. 为什么要使用线程池

我们为什么要使用线程池呢,主要有两个原因:

  1. 每次创建线程的时候都会占用一定的内存空间,如果要创建很多个线程,有可能会浪费内存,严重的情况下还有可能会导致内存溢出
  2. CPU 资源是有限的,同一时刻 CPU 只能处理一个线程,如果有大量的请求到达服务器,我们创建了大量的线程,那么很多线程都没有 CPU 的执行权,这些线程都需要等待获取 CPU 的执行权,在这个过程中会有非常多的切换线程操作,频繁的线程切换操作会导致上下文切换开销增大,CPU 花费在真正执行任务上的时间就会减少,从而导致程序的性能下降

在项目开发的过程中,一般都会用线程池来管理线程、创建线程

线程池相关的内容,跟实际开发是有很大关系的,面试官也是特别喜欢问

2. 线程池的核心参数和线程池的执行原理

在这里插入图片描述

2.1 线程池的核心参数

线程池的核心参数主要有七个,我们主要参考 ThreadPoolExecutor 类的具有七个参数的构造函数,这七个参数也是面试官提问的重点

在这里插入图片描述

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数(核心线程数 + 救急线程数的最大值)
  3. keepAliveTime:救急线程的生存时间,如果生存时间内没有新任务,与救急线程相关的资源会被释放
  4. unit:救急线程的生存时间单位
  5. workQueue:当没有空闲的核心线程时,新来任务会加入到此队列中排队,如果队列满了会创建救急线程来执行任务
  6. threadFactory:线程工厂,可以定制如何线程对象,例如为线程设置名字、是否为守护线程等
  7. handler:拒绝策略,当所有线程都在繁忙、workQueue 中的线程也满了时,会触发拒绝策略

2.2 线程池的执行原理

下面为大家介绍一下线程池的执行原理,也就是线程池是怎么工作的

在这里插入图片描述

首先,当有一个新任务到来时,会先判断核心线程数是否已经满了,如果核心线程数没满,就将任务添加到工作线程中,执行这个任务

如果核心线程数已经满了,会判断阻塞队列是否满了,如果阻塞队列中还有空间,就把任务添加到阻塞队列中进行等待,

如果阻塞队列满了,会判断线程数是否小于最大线程数,如果线程数小于最大线程数,会创建救急线程来执行任务

当核心线程或救急线程处于空闲的时候,会去阻塞队列中检查一下是否有需要执行的任务,如果有,就会使用核心线程或救急线程来执行阻塞队列中的任务


假如所有条件都不满足,线程池会有一个拒绝策略,拒绝策略有以下四种:

  1. AbortPolicy:直接抛出异常,默认使用的拒绝策略
  2. CallerRunsPolicy:用调用者所在的线程来执行任务
  3. DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
  4. DiscardPolicy:直接丢弃任务

可以运行以下代码,查看控制台的输出,辅助理解线程池的执行原理

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class TestThreadPoolExecutor {

    static class MyTask implements Runnable {

        private final String name;

        private final long duration;

        public MyTask(String name) {
            this(name, 0);
        }

        public MyTask(String name, long duration) {
            this.name = name;
            this.duration = duration;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " Running..." + this);
                Thread.sleep(duration);
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
        }

        @Override
        public String toString() {
            return "MyTask(" + name + ")";
        }
    }

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        ArrayBlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue<>(2);

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                3,
                0,
                TimeUnit.MILLISECONDS,
                arrayBlockingQueue,
                runnable -> new Thread(runnable, "myThread" + atomicInteger.getAndIncrement()),
                new ThreadPoolExecutor.AbortPolicy());
        // new ThreadPoolExecutor.CallerRunsPolicy());
        // new ThreadPoolExecutor.DiscardOldestPolicy());
        // new ThreadPoolExecutor.DiscardPolicy());
        showState(arrayBlockingQueue, threadPool);
        threadPool.submit(new MyTask("1", 3600000));
        showState(arrayBlockingQueue, threadPool);
        threadPool.submit(new MyTask("2", 3600000));
        showState(arrayBlockingQueue, threadPool);
        threadPool.submit(new MyTask("3"));
        showState(arrayBlockingQueue, threadPool);
        threadPool.submit(new MyTask("4"));
        showState(arrayBlockingQueue, threadPool);
        threadPool.submit(new MyTask("5", 3600000));
        showState(arrayBlockingQueue, threadPool);
        threadPool.submit(new MyTask("6"));
        showState(arrayBlockingQueue, threadPool);
    }

    private static void showState(ArrayBlockingQueue<Runnable> queue, ThreadPoolExecutor threadPool) {
        try {
            Thread.sleep(300);
        } catch (InterruptedException exception) {
            exception.printStackTrace();
        }
        List<Object> tasks = new ArrayList<>();
        for (Runnable runnable : queue) {
            try {
                Field callable = FutureTask.class.getDeclaredField("callable");
                callable.setAccessible(true);
                Object adapter = callable.get(runnable);
                Class<?> clazz = Class.forName("java.util.concurrent.Executors$RunnableAdapter");
                Field task = clazz.getDeclaredField("task");
                task.setAccessible(true);
                Object object = task.get(adapter);
                tasks.add(object);
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }
        System.err.println("pool size: " + threadPool.getPoolSize() + ", queue: " + tasks);
    }

}

如果运行代码时遇到了以下错误,运行前可以添加以下 VM 参数

错误信息:

java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.concurrent.Callable java.util.concurrent.FutureTask.callable accessible: module java.base does not "opens java.util.concurrent" to unnamed module @4fca772d
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)

VM 参数的具体内容:

--add-opens java.base/java.util.concurrent=ALL-UNNAMED 

3. 线程池中常见的阻塞队列

在这里插入图片描述

workQueue,当没有空闲的核心线程时,新来任务会加入到阻塞队列,阻塞队列满了后会创建救急线程执行任务

3.1 常见的阻塞队列

常见的阻塞队列主要有以下四种(重点了解 ArrayBlockingQueue 和 LinkedBlockingQueue):

  1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,符合队列的先进先出原则
  2. LinkedBlockingQueue:基于链表结构的有界阻塞队列,符合队列的先进先出原则
  3. DelayedWorkQueue:优先级队列,能够保证每次出队的元素是队列中延迟时间最短的任务
  4. SynchronousQueue:不存储元素的阻塞队列,每次执行插入操作前都必须等待一个移出操作

补充:与 DelayedWorkQueue 相关的知识

任何想要放入 DelayedWorkQueue 的对象都必须实现 Delayed 接口,Delayed 接口要求实现两个方法:

  1. getDelay(TimeUnit unit) 方法用于返回元素的剩余延迟时间,即距离执行时间还有多长时间
  2. compareTo(Delayed other) 方法用于比较两个 Delayed 对象的延迟时间

3.2 ArrayBlockingQueue 和 LinkedBlockingQueue 的区别

LinkedBlockingQueue(推荐使用)ArrayBlockingQueue
默认无界(Integer 类型的最大值),支持有界强制有界
底层的数据结构是链表底层的数据结构是数组
是懒惰的,创建节点的时候添加数据提前初始化 Node 数组
入队会生成新 NodeNode 需要提前创建好的
两把锁(链表的头部和尾部各一把锁)一把锁

在这里插入图片描述

4. 如何确定线程池的核心线程数

在这里插入图片描述

4.1 应用程序中任务的类型

应用程序中任务的类型主要可以分为两种:

  1. IO 密集型任务
  2. CPU 密集型任务

4.1.1 IO 密集型任务

常见的 IO 密集型任务主要有文件读写、数据库读写、网络请求等

4.1.2 CPU 密集型任务

常见的 CPU 密集型任务主要有计算较为密集的代码、BitMap 转换、JSON 转换等

4.2 如何确定核心线程数

对于 IO 密集型的任务来说,核心线程数可以设置为 2 * N + 1 (其中 N 为 CPU 的核数),因为 IO 密集型任务消耗的 CPU 资源较少

对于 CPU 密集型的任务来说,核心线程数可以设置为 N + 1 (其中 N 为 CPU 的核数),因为 CPU 密集型任务需要消耗大量的 CPU 资源,线程数少了,就能够减少 CPU 在不同线程之间切换所耗费的时间,充分地利用 CPU 资源

一般来说,用 Java 开发的应用程序,任务的类型大都为 IO 密集型


如何查看电脑的 CPU 核数呢,可以运行以下代码查看

public class ProcessorsDemo {

    public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());
    }

}

5. 线程池的种类

在这里插入图片描述

java.util.concurrent.Executors 类中提供了大量创建连接池的静态方法,常见的有四种:

  1. 创建使用固定线程数的线程池
  2. 创建单线程线程池
  3. 创建可缓存线程池
  4. 创建提供延迟功能和周期执行功能的线程池

5.1 固定线程数的线程池

我们可以查看创建固定线程数线程池的源码

在这里插入图片描述

固定线程数的线程池的特点是:

  1. 核心线程数与最大线程数一样,没有救急线程
  2. 阻塞队列是 LinkedBlockingQueue ,最大容量为 Integer.MAX_VALUE

固定线程数的线程池适用于任务量已知、耗时较长的任务

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolCase {

    static class FixedThreadDemo implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            for (int i = 0; i < 2; i++) {
                System.out.println(threadName + ":" + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建一个固定大小的线程池,核心线程数和最大线程数都是3
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executorService.submit(new FixedThreadDemo());
            Thread.sleep(10);
        }

        executorService.shutdown();
    }

}

5.2 单线程线程池

单线程线程池只会用唯一的工作线程来执行任务,保证所有任务按照先进先出的顺序(FIFO)执行

我们可以查看创建单线程线程池的源码

在这里插入图片描述

单线程线程池的特点是:

  1. 核心线程数和最大线程数都是1
  2. 阻塞队列是LinkedBlockingQueue ,最大容量为Integer.MAX VALUE

单线程线程池适用于需要严格按照顺序执行的任务

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadCase {

    static int count = 0;

    static class Demo implements Runnable {
        @Override
        public void run() {
            count++;
            System.out.println(Thread.currentThread().getName() + ":" + count);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 单个线程池,核心线程数和最大线程数都是1
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 10; i++) {
            executorService.execute(new Demo());
            Thread.sleep(5);
        }
        executorService.shutdown();
    }

}

5.3 可缓存线程池

我们可以查看创建可缓存线程池的源码

在这里插入图片描述

可缓存线程池的特点:

  1. 核心线程数为 0
  2. 最大线程数是 Integer.MAX VALUE
  3. 阻塞队列为 SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作

可缓存线程池适用于任务数比较密集,但每个任务执行时间较短的情况

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolCase {

    static class Demo implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                // 修改睡眠时间,模拟线程执行需要花费的时间
                Thread.sleep(100);

                System.out.println(threadName + "执行完了");
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建一个缓存的线程,没有核心线程数,最大线程数为Integer.MAX_VALUE
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Demo());
            Thread.sleep(1);
        }
        executorService.shutdown();
    }

}

5.4 提供延迟功能和周期执行功能的线程池

我们可以查看创建提供延迟功能和周期执行功能的线程池的源码

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


顾名思义,提供延迟功能和周期执行功能的线程池适用于需要延迟执行或周期执行的任务

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolCase {

    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                String threadName = Thread.currentThread().getName();

                System.out.println(threadName + ", 开始:" + new Date());
                Thread.sleep(1000);
                System.out.println(threadName + ", 结束:" + new Date());

            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 按照周期执行的线程池,核心线程数为2,最大线程数为Integer.MAX_VALUE
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
        System.out.println("程序开始:" + new Date());

        /*
         * schedule方法:提交任务到线程池中
         * 第一个参数:提交的任务
         * 第二个参数:任务执行的延迟时间
         * 第三个参数:时间单位
         */
        scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);
        scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);
        scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);

        Thread.sleep(5000);

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

}

6. 为什么不建议使用 Executors 类提供的静态方法创建线程池

在这里插入图片描述

阿里开发手册《Java开发手册-嵩山版》中指出

在这里插入图片描述

7. 线程池(多线程)的使用场景

在这里插入图片描述

7.1 CountDownLatch

CountDownLatch:闭锁、倒计时锁,用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

  1. 构造参数用来初始化等待计数值
  2. await()方法用来等待计数归零
  3. countDown()方法用来让计数减一

在这里插入图片描述

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        // 初始化了一个参数为 3 的倒计时锁
        CountDownLatch countDownLatch = new CountDownLatch(3);

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "-begin...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName() + "-end..." + countDownLatch.getCount());
        }).start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "-begin...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName() + "-end..." + countDownLatch.getCount());
        }).start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "-begin...");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName() + "-end..." + countDownLatch.getCount());
        }).start();

        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + "-waiting...");

        // 等待其他线程完成
        countDownLatch.await();
        System.out.println(threadName + "-wait end...");
    }

}

7.2 多线程使用场景一:ElasticSearch 批量导入数据

在项目上线之前,我们需要把数据库中的数据一次性的同步到 ElasticSearch 索引库中,但数据量达到百万级别时,一次性读取数据的做法是不可取的(Out Of Memory)

可以使用线程池的方式导入,利用 CountDownLatch 来控制就能避免一次性加载过多,防止内存溢出

在这里插入图片描述

7.3 多线程使用场景二:数据汇总

在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息

这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢


我们先来看一下常规的方案,先查询订单信息、再查询商品信息、最后查询物流信息,整个流程中每个部分是串行化执行的

在这里插入图片描述

我们先来看使用多线程的方案,查询订单信息、查询商品信息、查询物流信息三个操作相当于同时进行,整个流程中每个部分是并发执行的

在这里插入图片描述

当然,如果采用多线程的方案,需要使用 Future 接口(execute方法执行后会返回一个结果,结果的类型为 Future 接口)

7.4 多线程使用场景三:异步调用

在很多软件中都会有搜索功能,比如电商网站、地图软件等,并且这些软件会保存你的搜索记录

我们在实现搜索功能的时候,往往不会让保存搜索记录的操作影响到用户的正常搜索

在这里插入图片描述

我们可以选择采用异步线程来完成搜索记录的保存操作,具体要怎么操作呢?当用户开始搜索以后,我们正常返回与用户搜索内容相关的数据,用另一个线程去保存客户的搜索记录


那在代码中该如何实现呢(在 SpringBoot 项目中),只需要在保存用户搜索记录的具体方法上添加 @Async 注解

/**
 * 保存用户的搜索记录
 *
 * @param userId  Integer
 * @param keyword String
 */
@Async("taskExecutor")
@Override
public void insert(Integer userId, String keyword) {
    // 保存用户的搜索记录
    log.info("用户搜索记录保存成功,用户id:{},关键字:{}", userId, keyword);
}

同时 @Async 注解还能指定使用哪个线程池(该线程池需要由 Spring 管理)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@Configuration
public class ThreadPoolConfig {

    /**
     * 核心线程池大小
     */
    private static final int CORE_POOL_SIZE = 25;

    /**
     * 最大可创建的线程数
     */
    private static final int MAX_POOL_SIZE = 50;

    /**
     * 队列最大长度
     */
    private static final int QUEUE_CAPACITY = 1000;

    /**
     * 线程池维护线程所允许的空闲时间
     */
    private static final int KEEP_ALIVE_SECONDS = 500;

    @Bean("taskExecutor")
    public ExecutorService executorService() {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        LinkedBlockingQueue<Runnable> linkedBlockingQueue = new LinkedBlockingQueue<Runnable>(QUEUE_CAPACITY);
        return new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_SECONDS,
                TimeUnit.MILLISECONDS,
                linkedBlockingQueue,
                runnable -> new Thread(runnable, "wuyanzu-pool-" + atomicInteger.getAndIncrement()),
                new ThreadPoolExecutor.DiscardPolicy()
        );
    }

}

其中 ExecutorService 类是与线程池相关的类的顶层接口(IDEA 中按下 CTRL + H 快捷键可查看继承结构)

在这里插入图片描述

注意事项:如果想让 @Async 注解生效,需要在 SpringBoot 的启动类上添加 @EnableAsync 注解

8. 控制某个方法允许线程并发访问的线程数量(Semaphore)

在这里插入图片描述

Semaphore:信号量,JUC 包下的一个工具类,实现原理基于 AQS,可以通过 Semaphore 类限制执行的线程数量

Semaphore 通常用于那些资源有明确访问数量限制的场景,常用于限流

在这里插入图片描述

Semaphore的使用步骤:

  1. 创建 Semaphore 对象,并给定一个容量
  2. semaphore.acquire():请求一个信号量,这时候的信号量个数 - 1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
  3. semaphore.release():释放一个信号量,此时信号量个数 + 1

代码示例:

import java.util.concurrent.Semaphore;

public class SemaphoreCase {
    public static void main(String[] args) {
        // 1.创建 semaphore 对象
        Semaphore semaphore = new Semaphore(3);

        // 2.让 10 个线程同时运行
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    // 3. 获取许可,计数 - 1
                    semaphore.acquire();
                } catch (InterruptedException interruptedException) {
                    interruptedException.printStackTrace();
                }
                try {
                    System.out.println("running...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException interruptedException) {
                        interruptedException.printStackTrace();
                    }
                    System.out.println("end...");
                } finally {
                    // 4. 释放许可 计数 + 1
                    semaphore.release();
                }
            }).start();
        }
    }

}

9. ThreadLocal

在这里插入图片描述

ThreadLocal 是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本,从而解决了变量并发访问冲突的问题,ThreadLocal 同时实现了线程内的资源共享

例如,使用 JDBC 操作数据库时,会将每一个线程的 Connection 对象放入各自的 ThreadLocal 中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免 A 线程关闭了 B 线程的连接

9.1 ThreadLocal的基本使用

ThreadLocal 的基本使用:

  1. set(value):设置值
  2. get():获取值
  3. remove():清除值

代码示例:

public class ThreadLocalTest {

    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            threadLocal.set("Tom");
            removeAfterPrint(threadName);
            System.out.println(threadName + "-after remove : " + threadLocal.get());
        }, "t1").start();

        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            threadLocal.set("Jerry");
            removeAfterPrint(threadName);
            System.out.println(threadName + "-after remove : " + threadLocal.get());
        }, "t2").start();
    }

    static void removeAfterPrint(String str) {
        // 打印当前线程中本地内存中本地变量的值
        System.out.println(str + " : " + threadLocal.get());
        // 清除本地内存中的本地变量
        threadLocal.remove();
    }

}

9.2 ThreadLocal的实现原理&源码分析

ThreadLocal 本质来说就是一个线程内部存储类,让每个线程只操作自己内部的值,从而实现线程数据隔离

在这里插入图片描述

9.2.1 set 方法

在这里插入图片描述

9.2.2 get 方法

在这里插入图片描述

9.2.3 remove 方法

整体逻辑与 get 方法类似,找到目标元素后将其清除

在这里插入图片描述

在这里插入图片描述

9.3 ThreadLocal 的内存泄漏问题

在分析 ThreadLocal 的内存泄漏问题前,我们先来简单了解一下 Java 中的强引用和弱引用

强引用:最普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则 GC 并不会回收它。即使堆内存不足,宁可抛出 OOM(Out Of Memory) 错误,也不会对其进行回收

在这里插入图片描述

弱引用:表示一个对象处于可能有用且非必须的状态,在 GC 线程扫描内存区域时,一旦发现弱引用,就会回收与弱引用相关联的对象。对于弱引用的回收,无论内存区域是否足够,一旦发现就会回收

在这里插入图片描述

每一个 Thread内部都维护一个了 ThreadLocalMap ,在 ThreadLocalMap 中的 Entry 对象继承了 WeakReference ,其中 key 为使用弱引用的 ThreadLocal 实例,value 为线程变量的副本


以下是 ThreadLocal 类的部分源码

在这里插入图片描述

那怎么样防止内存泄漏呢,非常简单,就是在用完 ThreadLocal 类之后,主动调用 ThreadLocal 类的 remove 方法,把数据清理掉,就能避免内存泄露的情况了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1996862.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

开源AI智能名片小程序在私域流量运营中的“及时法则”深度应用与策略探讨

摘要&#xff1a;在数字化浪潮的推动下&#xff0c;私域流量已成为企业构建长期竞争优势的关键要素。开源AI智能名片小程序&#xff0c;凭借其智能化、个性化及高度可定制化的特性&#xff0c;正逐步成为私域流量运营的重要工具。本文深入探讨了“及时法则”在开源AI智能名片小…

模型量化——NVIDIA——QAT

概述 QAT 截止目前(20230418)的CUDA 实现并不在pytorch 原生包中(不等同于pytorch 的QAT,它主要支持CPU),需要引入NVIDIA 的第三方包“pytorch-quantization”。需要TRT8+ 、 pytorch 1.8 +。主要流程如下: 工具流转方向如下: 所以目前我的理解+咨询了NVIDIA官…

【代码随想录】螺旋矩阵II

本博文为代码随想录的学习笔记&#xff0c;原文链接&#xff1a;代码随想录 题目 原题链接&#xff1a;59. 螺旋矩阵 II 给你一个正整数 n &#xff0c;生成一个包含 1 到 n^2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xf…

【Linux】lvm被删除或者lvm丢失了怎么办

模拟案例 接下来模拟lvm误删除如何恢复的案例&#xff1a; 模拟删除&#xff1a; 查看vg名&#xff1a; vgdisplayvgcfgrestore --list uniontechos #查看之前的操作 例如我删除的&#xff0c;现场没有删除就用最近的操作文件&#xff1a; 还原&#xff1a; vgcfgrestore…

1Panel应用推荐:KubePi开源Kubernetes管理面板

1Panel&#xff08;github.com/1Panel-dev/1Panel&#xff09;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;它致力于通过开源的方式&#xff0c;帮助用户简化建站与运维管理流程。为了方便广大用户快捷安装部署相关软件应用&#xff0c;1Panel特别开通应用商店&am…

扩展02:Haporxy+Keepalived+Mysql高可用集群实战

由于这个架构和扩展01的大致步骤都相同&#xff0c;就不讲解了。看如下图再参考扩展01即可。

用C语言实现链式存储结构 万字

各位同学&#xff0c;大家好&#xff0c;我叫小敖。今天给大家分享数据结构之一链式存储结构&#xff0c;下面是对链表简单介绍&#xff0c;希望大家能理解。 链表介绍 链表是一种物理存储单元上非连续、非顺序的存储结构**&#xff0c;数据元素的逻辑顺序是通过链表中的指针链…

大模型快速部署,以浪潮源2.0为例

step1: 申请PAI-DSW试用 step2&#xff1a;魔塔社区授权 由于本地授权一直失败&#xff0c;于是采用了魔塔免费平台实例进行学习。 搭建好之后&#xff0c;打开就有相关页面了&#xff1a; demo搭建&#xff1a; 按照官方提示的步骤进行搭建&#xff0c;内容如下&#xff1a;…

第二十一节、敌人追击状态的转换

一、物理检测中的Boxcast 1、检测敌人Bool 当不知道一个函数的返回值是什么的时候 定义一个var变量 就知道了 二、状态切换 1、switch用法 2、新的语法糖写法

ubuntu2004上的glib编译教程

最近因为工作需要编译glib&#xff0c;写文章记录一下编译流程。 从launchpad上下载源码&#xff1a;链接 根据control文件的提示安装对应的依赖 然后尝试debuild&#xff0c;这里会编译不过出去&#xff0c;不过debuild会自动生成一些编译文件&#xff0c;不要删除。 接下来…

2024云南导游资格证题库

1、释迦三圣是( )。 A、药师佛 B、文殊菩萨 C、释迦牟尼 D、普贤菩萨 E、观音菩萨 答案&#xff1a;BCD 2、下列女士套裙穿法中&#xff0c;错误的是&#xff08; &#xff09;。 A、上衣的袖长不超过着装者的手腕&#xff0c;裙子不盖过脚踝 B、女士在正式场合穿…

八、MyBatis

一、MyBatis介绍 MyBatis 是持久层框架&#xff0c;它支持自定义 SQL、存储过程以及⾼级映射。MyBatis 去除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置 和映射原始类型、接口和 Java POJO&#xff08;Plain Old Java Obj…

数据结构--第七天

递归 -递归的概念 递归其实就是一种解决问题的办法&#xff0c;在C语言中&#xff1a;递归就是函数自己调用自己 -递归的思想 递归的思考方式就是把大事化小的过程 递归的递就是递推的意思&#xff0c;归就是回归的意思 &#xff08;递归是少量的代码完成大量的运算&#xff09…

数据结构(5.5_2)——并查集

逻辑结构——数据元素之间的逻辑关系 并查集&#xff1a; 并查集&#xff08;Union-Find&#xff09;是一种树型的数据结构&#xff0c;用于处理一些不交集的合并及查询问题。它支持两种操作&#xff1a; 用双亲表示存储并查集 首先将所有根节点数组值设为-1&#xff0c;其…

[Android] [解决]Bottom Navigation Views Activity工程带来的fragment底部遮盖的问题

创建了Bottom Navigation Views Activity之后&#xff0c;在fragment_home.xml&#xff0c;加了一个RecyclerView&#xff0c; 后来添加了item之后发现底部会被盖住一部分。 解决&#xff1a;在layout里面加两句&#xff1a; android:paddingBottom"?attr/actionBarSize&…

C#获取Network的相关信息

1&#xff0c;获取网络的通断。 //方法1&#xff1a;无效果&#xff0c;并不能反映当前网络通断 bool availableSystem.Windows.Forms.SystemInformation.Network//方法2&#xff1a;通过VB获取网络状态&#xff0c;可反映当前网络通断 Microsoft.VisualBasic.Devices.Network…

Qt QML 使用QPainterPath绘制弧形曲线和弧形文本

Qt并没有相关api直接绘制弧形文字&#xff0c;但提供了曲线绘制相关类&#xff0c;所以只能另辟蹊径&#xff0c;使用QPainterPath先生成曲线&#xff0c;然后通过曲线上的点来定位每个文字并draw出来。 QML具体做法为从QQuickPaintedItem继承&#xff0c;在派生类中实现paint…

Linux 系统框架分析(一)

一、linux内核结构框图 对内核结构框图有个总体的把握&#xff0c;有助于理解为什么驱动要这样写&#xff0c;为什么写的应用程序所用的C库接口能够产生这么多的事情。 框图可以看出来&#xff0c;linux系统&#xff0c;包括五个系统 一、Linux内核结构介绍 Linux 内核是操作…

【机器学习基础】线性回归

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈Python机器学习 ⌋ ⌋ ⌋ 机器学习是一门人工智能的分支学科&#xff0c;通过算法和模型让计算机从数据中学习&#xff0c;进行模型训练和优化&#xff0c;做出预测、分类和决策支持。Python成为机器学习的首选语言&#xff0c;…

Flutter 生成图表(fl_chart)

Flutter 图表 使用fl_chart 先看最终效果 实现抖音’使用管理助手’效果 需求分析&#xff1a;统计每个用户近7天每天的使用时长&#xff08;从当天往后推导&#xff0c;假设今天周二&#xff09;&#xff0c;单位为分钟或者小时&#xff0c;根据平均时长决定 技术选型&…