Java并发编程学习16-线程池的使用(中)

news2024/11/17 7:34:00

线程池的使用(中)

  • 引言
  • 1. 配置 ThreadPoolExecutor
    • 1.1 线程的创建与销毁
    • 1.2 管理队列任务
    • 1.3 饱和策略
    • 1.4 线程工厂
    • 1.5 定制 ThreadPoolExecutor
  • 2. 扩展 ThreadPoolExecutor
  • 总结

引言

上篇分析了在使用任务执行框架时需要注意的各种情况,并简单介绍了如何正确调整线程池大小。

在这里插入图片描述

本篇将继续介绍对线程池进行配置与调优的一些方法,详细如下:

1. 配置 ThreadPoolExecutor

ThreadPoolExecutorExecutors 中的 newCachedThreadPoolnewFixedThreadPoolnewScheduledThreadExecutor 等工厂方法返回的 Executor 提供了基本的实现,它是个灵活、稳定的线程池,允许开发人员进行各种定制,以满足需求。

如果默认的执行策略不能满足要求,那我们该如何定制呢?

可以通过 ThreadPoolExecutor 的构造函数来实例化一个对象,并根据实际的需要来定制,具体可以参考 Executors 的源码来了解默认配置下的执行策略,然后再以这些执行策略为基础进行修改。

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

1.1 线程的创建与销毁

线程池中,线程的创建与销毁都有哪些因素来共同负责呢?

  • 线程池的基本大小(corePoolSize)。也就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。

  • 线程池的最大大小(maximumPoolSize)。可同时活动的线程数量的上限。

  • 线程的存活时间(keepAliveTime)。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。

通过调节线程池的基本大小和存活时间,可以帮助线程池回收空闲线程占有的资源,从而使得这些资源可以用于执行其他工作。

下面我们来看看 线程池框架 Executors 中的工厂方法是如何调节的?

  • newFixedThreadPool 工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且创建的线程池不会超时。

  • newCachedThreadPool 工厂方法将线程池的最大大小设置为 Integer.MAX_VALUE,而将基本大小设置为 ,并将超时设置为 1 分钟,这种方法创建出来的线程池可以被无限扩展,并且当需求降低时也会自动收缩。

当然,其他形式的线程池可以通过显式的 ThreadPoolExecutor 构造函数来灵活地构造。

1.2 管理队列任务

在笔者的《Java并发编程学习10-任务执行与Executor框架》这篇博文中提到过,如果无限制地创建线程,那么不仅带来高的资源消耗,也增加了系统的不稳定性。

虽然通过采用固定大小的线程池可以解决上述问题,但在高负载情况下,应用程序仍可能耗尽资源,比如新请求的到达速率超过了线程池的处理速率。即使请求的平均到达速率很稳定,也仍然会出现请求突增的情况。我们知道,尽管队列有助于缓解任务的突增问题,但如果任务持续高速地到来,那么最后还是要抑制请求的到达率以避免耗尽内存。甚至在耗尽内存之前,响应性能也将随着任务队列的增长而变得越来越糟糕。

ThreadPoolExecutor 中,它提供了一个 BlockingQueue 来保存等待执行的任务。

基本的任务排队方法有 3 种:

  • 无界队列
  • 有界队列
  • 同步移交

newFixedThreadPoolnewSingleThreadExecutor 在默认情况下将使用一个无界的 LinkedBlockingQueue 来进行队列的管理。如果所有工作者线程都处于忙碌状态,那么任务将在队列中等候。如果任务持续快速地到达,并且超过了线程池处理它们的速度,那么队列将无限制地增加。

那有没有一种更稳妥的资源管理策略呢?

那当然是有的,那就是使用有界队列,例如 ArrayBlockingQueue、有界的 LinkedBlockingQueuePriorityBlockingQueue

虽然有界队列有助于避免资源耗尽的情况发生,但 当队列填满之后,新的任务该怎么办呢?

这就要不得不提到 饱和策略[Saturation Policy],详见下面的 1.3 小节

注意:在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。如果线程池较小而队列较大,那么有助于减少内存使用量,降低 CPU 的使用率,同时还可以减少上下文切换,但付出的代价就是会限制系统的吞吐量。

对于非常大的或者无界的线程池,我们该如何来避免任务排队呢?

可以通过使用 SynchronousQueue,直接将任务从生产者移交给工作者线程。

SynchronousQueue 不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入 SynchronousQueue 中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么 ThreadPoolExecutor 将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。

使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被首先放在队列中,然后由工作者线程从队列中提取该任务。

只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue 才有实际意义。在 newCachedThreadPool 工厂方法中就使用了它。

对于 ExecutornewCachedThreadPool 工厂方法是一种很好的默认选择,它能提供比固定大小的线程池更好的排队性能。这种性能的差异是由于使用了 SynchronousQueue 而不是 LinkedBlockingQueue。当需要限制当前任务的数量以满足资源管理需求时,那么可以选择固定大小的线程池。

LinkedBlockingQueueArrayBlockingQueue 这样的 FIFO(先进先出)队列,任务的执行顺序与它们的到达顺序相同。如果你想控制任务的执行顺序,就可以使用 PriorityBlockingQueue,这个队列将根据优先级来安排任务。

那这里的任务优先级是通过什么来定义的呢?

  • 自然顺序 :任务本身具有可以比较大小的能力,且它实现了 Comparable 接口,并在 compareTo() 方法中定义任务之间的优先级关系。
  • Comparator:任务本身不具备比较大小的能力,则它可以使用自定义的 Comparator 对象来定义任务之间的优先级关系

注意:只有当任务相互独立时,为线程池或者工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程 ”饥饿“ 死锁问题。

1.3 饱和策略

上文其实我们已经提到了,当有界队列被填满后,线程池的饱和策略将开始发挥作用。

那什么是线程池的饱和策略呢?

线程池的饱和策略是指当线程池无法处理新提交的任务时,如何拒绝这些任务并通知提交者。

Java 提供了哪些内置的饱和策略呢?

  • AbortPolicy【中止策略】: 默认的饱和策略,当线程池无法处理新提交的任务时,直接抛出 RejectedExecutionException 异常;调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
  • CallerRunsPolicy【调用者运行策略】: 当线程池无法处理新提交的任务时,直接在提交任务的线程中执行该任务;
  • DiscardPolicy【抛弃策略】: 当线程池无法处理新提交的任务时,直接丢弃当前任务。
  • DiscardOldestPolicy【抛弃最旧的策略】: 当线程池无法处理新提交的任务时,丢弃队列中最旧的未处理任务,并尝试重新提交当前任务;

除了上述的四种内置的饱和策略外,我们还可以通过实现 RejectedExecutionHandler 接口来自定义饱和策略,具体如下:

public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义饱和策略的处理逻辑
    }
}

然后在创建线程池时,可以通过其构造方法传递自定义的饱和策略,具体如下:

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(10), new MyThreadFactory(), new MyRejectedExecutionHandler());

当然创建线程池时,也是可以使用 setRejectedExecutionHandler 方法来给线程池设置新的饱和策略。

下面演示创建一个固定大小的线程池,并采用有界队列以及 “调用者运行” 饱和策略,具体如下:

ThreadPoolExecutor executor = new ThreadPoolExecutor(N_THREADS, N_THREADS, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(CAPACITY));

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

当工作队列被填满后,实际上是没有预定义的饱和策略来阻塞任务的提交。不过,我们可以使用 Semaphore(信号量)来控制任务的提交速率,下面来看一下如下的实例:

@ThreadSafe
public class BoundedExecutor {
    private final Executor exec;
    
    private final Semaphore semaphore;

    public BoundedExecutor(Executor exec, int bound) {
        this.exec = exec;
        this.semaphore = new Semaphore(bound);
    }
    
    public void submitTask(final Runnable command) throws InterruptedException {
        semaphore.acquire();
        try {
            exec.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            semaphore.release();
        }
    }
}

在上述的 BoundedExecutor 类中,我们使用信号量来控制正在执行的和等待执行的任务数量,其中信号量的上界需要设置为 线程池的大小 加上 可排队的任务数量

1.4 线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。线程池中默认的线程工厂方法将创建一个新的、非守护的线程,并且不包含特殊的配置信息。

ThreadFactory 中只定义了一个方法 newThread,每当线程池需要创建一个新线程时都会调用这个方法。如下可见 ThreadFactory 接口:

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

当然很多情况下,我们需要使用定制的线程工厂方法,比如:

  • 为线程池的线程指定一个 UncaughtExecptionHandler
  • 实例化一个定制的 Thread 类用于执行调式信息的记录;
  • 为线程指定名字,用来解释线程的转储信息和错误日志。

那么我们可以通过指定一个线程工厂方法,来定制线程池的配置。

下面给出一个自定义的线程工厂示例:

public class MyThreadFactory implements ThreadFactory {
    private final String poolName;

    public MyThreadFactory(String poolName) {
        this.poolName = poolName;
    }

    @Override
    public Thread newThread(Runnable runnable) {
        return new MyAppThread(runnable, poolName);
    }
}

当然在 MyAppThread 中,我们还可以定制其他行为,包括:为线程指定名字,设置自定义 UncaughtExecptionHandlerLogger 中写入信息,维护一些同级信息(包括有多少个线程被创建和销毁),以及在线程被创建或者终止时把调试信息写入日志。

下面来看一下具体的示例:

public class MyAppThread extends Thread {

    public static final String DEFAULT_NAME = "MyAppThread";

    private static volatile boolean debugLifecycle = false;

    private static final AtomicInteger created = new AtomicInteger();

    private static final AtomicInteger alive = new AtomicInteger();

    private static final Logger LOGGER = Logger.getAnonymousLogger();

    public MyAppThread(Runnable runnable) {
        this(runnable, DEFAULT_NAME);
    }

    public MyAppThread(Runnable runnable, String poolName) {
        super(runnable, poolName + "-" + created.incrementAndGet());
        setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                LOGGER.log(Level.SEVERE, "UNCAUGHT in thread " + t.getName(), e);
            }
        });
    }

    @Override
    public void run() {
        // 复制 debug 标志以确保一致的值
        boolean debug = debugLifecycle;
        if (debug)
            LOGGER.log(Level.FINE, "Created " + getName());
        try {
            alive.incrementAndGet();
            super.run();
        } finally {
            alive.decrementAndGet();
            if (debug)
                LOGGER.log(Level.FINE, "Exiting " + getName());
        }
    }

    public static int getThreadsCreated() {
        return created.get();
    }

    public static int getThreadsAlive() {
        return alive.get();
    }

    public static boolean getDebug() {
        return debugLifecycle;
    }

    public static void setDebug(boolean b) {
        debugLifecycle = b;
    }
}

Java 应用程序中,如果需要通过安全策略来控制对某些特殊代码库的访问权限,可以使用 Java 安全管理器(SecurityManager)来实现。Java 安全管理器是一个用于保护 Java 应用程序免受恶意代码攻击的重要组件,它通过限制应用程序对系统资源和敏感信息的访问,防止可能存在的安全漏洞被利用。其中在 Executors 中的 privilegedThreadFactory 工厂方法获取的线程工厂就使用了 Java 安全管理器的机制。

如果不使用 privilegedThreadFactory,线程池创建的线程将从在需要新线程时调用 executesubmit 的客户程序中继承访问权限,从而导致一些安全性的问题。例如,如果客户程序的访问权限范围比较广泛,而某个特定的代码库只能被授权用户访问,那么此时在客户程序中创建的线程就可能会绕过安全策略,从而访问到受保护的代码库,导致安全漏洞。

因此,在需要进行安全控制的情况下,推荐使用 privilegedThreadFactory 工厂方法来创建线程池。通过这种方式创建出来的线程,将与创建 privilegedThreadFactory 的线程拥有相同的访问权限、AccessControlContextcontextClassLoader,从而确保了线程池中的所有线程都受到相应的安全策略的保护。这样,就能够更好地保障系统的安全性和稳定性。

1.5 定制 ThreadPoolExecutor

在调用完 ThreadPoolExecutor 的构造函数后,仍然可以通过设置函数来修改大多数传递给它的构造函数的参数(例如线程池的基本大小、最大大小、存活时间、线程工厂以及拒绝执行处理器)。

如果 Executor 是通过 Executors 中的某个工厂方法创建的(newSingleThreadExecutor除外),那么可以将结果的类型转换为 ThreadPoolExecutor ,然后再进行设置。

下面来看一下具体的示例:

	ExecutorService exec = Executors.newCachedThreadPool();
	if (exec instanceof ThreadPoolExecutor)
		((ThreadPoolExecutor) exec).setCorePoolSize(10);
	else
		throw new AssertionError("Oops, bad assumption");

Executors 中包含一个 unconfigurableExecutorService 工厂方法,该方法对一个现有的 ExecutorService 进行包装,使其只暴露出 ExecutorService 的方法,因此不能对它进行配置。

另外 newSingleThreadExecutor 工厂方法,也返回按这种方式封装的 ExecutorService,而不是最初的 ThreadPoolExecutor,所以也不能对它进行配置。

知识点:我们可以在自己的 Executor 中使用上面这种方式以防止执行策略被修改。如果将 ExecutorService 暴露给不信任的代码,有不希望对其进行修改,就可以通过 unconfigurableExecutorService 来包装它。

2. 扩展 ThreadPoolExecutor

ThreadPoolExecutor 是可扩展的,它提供了几个可以在子类中改写的方法:beforeExecuteafterExecuteterminated,这些方法可以用于扩展 ThreadPoolExecutor 的行为。

在执行任务的线程中将调用 beforeExecuteafterExecute 等方法,在这些方法中还可以添加日志、计时、监视或统计信息收集的功能。无论任务是从 run 中正常返回,还是抛出一个异常返回,afterExecute 都会被调用(如果任务完成后带有一个 Error,那么就不会调用 afterExecute)。如果 beforeExecute 抛出一个 RuntimeException,那么任务将不被执行,并且 afterExecute 也不会被调用。

在线程池完成关闭操作时调用 terminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后。 terminated 可以用来释放 Executor 在其生命周期内分配的各种资源,此外还可以执行发送通知、记录日志或者收集 finalize 统计信息等操作。

下面我们来看一下如下的示例【给线程池添加统计信息】:

public class TimingThreadPool extends ThreadPoolExecutor {
    private final ThreadLocal<Long> startTime = new ThreadLocal<>();

    private final Logger LOGGER = Logger.getLogger("TimingThreadPool");

    private final AtomicLong numTasks = new AtomicLong();

    private final AtomicLong totalTime = new AtomicLong();

    public TimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

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

    public TimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public TimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        LOGGER.fine(String.format("Thread %s: start %s", t, r));
        startTime.set(System.nanoTime());
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        try {
            long endTime = System.nanoTime();
            long taskTime = endTime - startTime.get();
            numTasks.incrementAndGet();
            totalTime.addAndGet(taskTime);
            LOGGER.fine(String.format("Thread %s: end %s, time= %dns", t, r, taskTime));
        } finally {
            super.afterExecute(r, t);
        }

    }

    @Override
    protected void terminated() {
        try {
            LOGGER.info(String.format("Terminated: avg time=%dns", totalTime.get() / numTasks.get()));
        } finally {
            super.terminated();
        }
    }
}

上面的 TimingThreadPool 中给出了一个自定义的线程池,它通过 beforeExecuteafterExecuteterminated 等方法来添加日志记录和统计信息收集。为了测量任务的运行时间, beforeExecute 必须记录开始时间并把它保存到一个 afterExecute 可以访问的地方。因为这些方法将在执行任务的线程中调用,因此 beforeExecute 可以把值保存到一个 ThreadLocal 变量中,然后由 afterExecute 来读取。另外,在 TimingThreadPool 中使用了两个 AtomicLong 变量,分别用于记录已处理的任务数和总的处理时间,并通过 terminated 来输出包含平均任务时间的日志消息。

总结

本篇介绍了 ThreadPoolExecutor 配置和扩展相关的信息,相关示例代码请访问 GitHub:thread-pool-demo ;

下一篇《线程池的使用》最后一篇,将介绍递归算法的并行化改造,其中会对我们《Java并发编程学习11-任务执行Demo》介绍的页面绘制程序进行一系列改进,敬请期待!!!

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

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

相关文章

死锁的成因以及解决方案(简析)

目录 一.为什么会产生死锁? 二.死锁产生的几个场景 一个线程一把锁的情况 关于可重入和不可重入锁的简单举例 两个线程两把锁的情况 多线程多把锁 如何解决死锁 一.为什么会产生死锁? 简单来说,就是进程加锁之后,没有被解锁而处于一直等待的状态 二.死锁产生的几个场景…

深入理解深度学习——BERT(Bidirectional Encoder Representations from Transformers):BERT的结构

分类目录&#xff1a;《深入理解深度学习》总目录 相关文章&#xff1a; BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;&#xff1a;基础知识 BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09…

软件架构模式—分层架构

这是软件架构模式博客系列第 2 章&#xff0c;我们将讨论分层架构模式。 分层架构模式是一种n层模式&#xff0c;其中组件按照水平层次进行组织。这是设计大多数软件的传统方法&#xff0c;旨在实现自我独立。这意味着所有组件之间相互连接&#xff0c;但彼此之间不相互依赖。…

测试体系与测试方案设计

如果我们想要测试一个系统&#xff0c;我们得先需要了解被测系统架构 业务架构:业务模型分析技术架构:技术组件、通讯协议分析数据架构:数据模型、数据存储引擎分析 电子商城 Mall 开源项目技术架构 经典技术架构 网关产品 Nginx Apache HttpdWeb 应用开发 Vue.js React移动应…

福州大学学报退稿率【爬虫+数据处理】

目录 一、爬虫 二、数据处理 2.1 历年投稿总数&#xff1a; 2.2 各稿件状态比例&#xff1a; 2.3 历年退稿率 三、总结&#xff08;福州大学学报退稿率&#xff09; 一、爬虫 从福州大学学报微信公众号可以发现稿件状态的查询接口&#xff0c; 根据测试可知稿件号由年份与当…

Linux共享内存

博客内容&#xff1a;共享内存 文章目录 一、认识共享内存结构二、如何创建共享内存&#xff1f;1.创建共享内存2.关联进程&#xff0c;取消进程3.释放共享内存 三、代码示例总结 一、认识共享内存结构 共享内存 共享内存指 (shared memory)在多处理器的计算机系统中&#xff…

新手速成!如何使用ChatGPT成为你的导师

1. 写在前面 最近我发现咱们的团队现在是人手ChatGPT&#xff0c;不光是我们团队&#xff0c;我整个行业的人都在用它解决生活跟工作中遇到的问题。可以看到的是大家也都是对它赞赏度很高 本文我将为大家介绍如何更加高效的使用ChatGPT提高工作效率&#xff0c;面向ChatGPT编程…

JavaScript高级学习总结

函数作用域 函数内部声明的变量&#xff0c;在函数外部无法被访问函数的参数也是函数内部的局部变量不同函数内部声明的变量无法互相访问函数执行完毕之后&#xff0c;函数内部的变量实际被清空了 块作用域 let声明的变量会产生块作用域&#xff0c;var不会产生块作用域cons…

QT +OpenSSL配置

QT OpenSSL配置 1 查看自己QT支持的OPenSSL版本号1.1 查看版本号1.2 是否配置了OPenSSL 2 安装OPenSSL2.1 下载已经编译好的库2.2 自己编译代码2.2.1 下载perl2.2.1 下载OPenSSL源码 1 查看自己QT支持的OPenSSL版本号 1.1 查看版本号 新建项目testOpenSSLpro文件中加入QT ne…

(贪心) 649. Dota2 参议院 ——【Leetcode每日一题】

❓ 649. Dota2 参议院 难度&#xff1a;中等 Dota2 的世界里有两个阵营&#xff1a;Radiant&#xff08;天辉&#xff09;和 Dire&#xff08;夜魇&#xff09; Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过…

Debian11 dhclient 不自动执行问题

这两天用U盘安装Debian11&#xff0c;在”安装软件“一直提示失败&#xff0c;但可以跳过这一步继续往下安装&#xff0c;好在基本系统及grub能正常安装&#xff0c;最后系统也能正常起来了&#xff0c;但发现系统起来后没有ip地址&#xff0c;需要手动执行 dhclient 来获取ip。…

Java的第十二篇文章——集合

目录 第十二章 集合 学习目标 1. 集合框架的由来 2. 集合框架的继承体系 3. Collection接口 3.1 Collection接口的常用方法 4. Iterator接口 4.1 Iterator接口的抽象方法 4.2 获取迭代器接口实现类 4.3 迭代器的实现原理 4.4 并发修改异常 4.5 集合存储自定义对象并…

【Git常用命令及在IDEA中的使用】

Git常用命令及在IDEA中的使用 Git常用命令及在IDEA中的使用1 Git 概述1.1 Git 简介1.2 Git 下载与安装 2 Git 代码托管服务2.1 常用的Git 代码托管服务2.2 使用码云代码托管服务 3 Git 常用命令3.1 Git 全局设置3.2 获取 Git 仓库3.3 工作区、暂存区、版本库 概念3.4 Git工作区…

MyBatis面试题总结

1.概念/使用方法向的问题 1.1 什么是Mybatis? &#xff08;1&#xff09;Mybatis是一个半ORM框架&#xff0c;它内部封装了JDBC&#xff0c;开发时只需要关注SQL语句本身&#xff0c;不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。 &#xff08;2&a…

​​​​SpringBoot 监控神器——Actuator 保姆级教程

pom.xml info beans conditions heapdump shutdown mappings threaddump loggers 端点 metrics 端点 自定义Endpoint 自定义监控端点常用注解 使用Filter对访问actuator做限制 Spring Boot Monitor做监控页面 SpringBoot自带监控功能Actuator&#xff0c;可以帮助…

Kubernetes学习笔记-kubernetes应用扩展(2)-使用kubernetes服务目录扩展kubernetes20230623

一、服务目录介绍 服务目录就是列出所有的服务的目录。用户可以浏览目录并自行设置目录中列出的服务实例&#xff0c;无须处理服务运行所需的pod、service、configmap和其他资源。这听起来和自定义网站资源很类似。 服务目录并不会为每种服务类型的api服务器添加自定义资源&a…

全栈开发实战那些事

文章目录 一个网站是怎么来的&#xff1f; Git篇隔离项目和原有Git工程联系Git冲突的原因通常有以下几种&#xff1a; IDEA篇IDEA常用操作Git可视化操作&#xff08;提交代码前先pull更新merge最新版本一下再push&#xff0c;保证提交的最终项目是最新&#xff09; IDEA中Git冲…

Jenkins 发送文件到远程服务器:Publish Over SSH 插件

Jenkins 发送文件到远程服务器&#xff1a;Publish Over SSH 插件 文章目录 Jenkins 发送文件到远程服务器&#xff1a;Publish Over SSH 插件一、Publish Over SSH 插件1、概述2、主要功能和特点3、插件主页4、安装 Publish Over SSH 插件5、配置远程主机 二、发送文件到远程主…

Python基础篇(七):面向对象的编程思想

面向对象 前言1. 面向对象编程思想1.1 面向对象的相关概念1.2 面向对象的三大特性 2. 类的定义2.1 使用class关键字定义类2.2 创建类对象并调用属性和方法 3.面向对象思想示例3.1 详细示例&#xff1a;图形类的设计3.2 基类 Shape3.3 子类 Circle3.4 子类 Rectangle3.5 使用图形…

【Java-SpringBoot+Vue+MySql】Day5-前端进阶

目录 一、Axios网络请求 中文文档&#xff1a; 安装&#xff1a; 导入&#xff1a; 使用方法&#xff1a; 基本语法&#xff1a; 生命周期函数&#xff1a; 二、前端路由VueRouter 视频&#xff1a;12.前端路由VueRouter_哔哩哔哩_bilibili 参考文档: 三、状态管理VueX …