JUC线程池的实战问题引出的一系列原理问题

news2025/2/28 7:42:32

1 我们为什么需要使用线程池

线程过多会带来额外的开销,其中包括创建销毁线程的开销调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待、监督、管理、分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。线程池是一种通过“池化”思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类。

2 Executors [ɪgˈzɛkjətərz] 创建四种常见线程池

2.1 newFixedThreadPool

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

从构造方法可以看出,它创建了一个固定大小的线程池,每次提交一个任务就创建一个线程,直到线程数达到线程池的最大值nThreads。线程池的大小一旦达到最大值后,再有新的任务提交时则放入阻塞队列中,等到有线程空闲时,再从队列中取出任务继续执行。FixedThreadPool提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

package com.zs.thread;
public class TestVolatile {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                        System.out.println("运行时间: " + sdf.format(new Date()) + " " + index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        fixedThreadPool.shutdown();
    }
}

例中创建了一个固定大小为3的线程池,然后在线程池提交了5个任务。在提交第4个任务时,因为线程池的大小已经达到了3并且前3个任务在运行中,所以第4个任务被放入了队列,等待有空闲的线程时再被运行。运行结果如下(注意前3个任务和后2个任务的运行时间):
在这里插入图片描述

2.2 newCachedThreadPool

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

从构造方法可以看出,它创建了一个可缓存的线程池。当有新的任务提交时,有空闲线程则直接处理任务,没有空闲线程则创建新的线程处理任务,队列中不储存任务。线程池不对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。如果线程空闲时间超过了60秒就会被回收。在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。

package com.zs.thread;

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

        for (int i = 0; i < 5; i++) {
            final int index = i;
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat(
                                "HH:mm:ss");
                        System.out.println("运行时间: " +
                                sdf.format(new Date()) + " " + index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        cachedThreadPool.shutdown();
    }
}

因为这种线程有新的任务提交,就会创建新的线程(线程池中没有空闲线程时),不需要等待,所以提交的5个任务的运行时间是一样的。
在这里插入图片描述

2.3 newSingleThreadExecutor

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

从构造方法可以看出,它创建了一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

package com.zs.thread;

public class TestVolatile {
    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 5; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat(
                                "HH:mm:ss");
                        System.out.println("运行时间: " +
                                sdf.format(new Date()) + " " + index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        singleThreadExecutor.shutdown();
    }
}

因为该线程池类似于单线程执行,所以先执行完前一个任务后,再顺序执行下一个任务:
在这里插入图片描述
既然类似于单线程执行,那么这种线程池还有存在的必要吗?这里的单线程执行指的是线程池内部,从线程池外的角度看,主线程在提交任务到线程池时并没有阻塞,仍然是异步的。

2.4 newScheduledThreadPool

这个方法创建了一个固定大小的线程池,支持定时及周期性任务执行。

package com.zs.thread;

public class TestVolatile {
    public static void main(String[] args) {
        final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        System.out.println("提交时间: " + sdf.format(new Date()));
        scheduledThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("运行时间: " + sdf.format(new Date()));
            }
        }, 3, TimeUnit.SECONDS);
        scheduledThreadPool.shutdown();
    }
}

使用该线程池的schedule方法,延迟3秒钟后执行任务,运行结果如下:
在这里插入图片描述
再看一下周期执行的例子:

package com.zs.thread;

public class TestVolatile {
    public static void main(String[] args) throws InterruptedException {
        final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        System.out.println("提交时间: " + sdf.format(new Date()));
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("运行时间: " + sdf.format(new Date()));
            }
        }, 1, 3, TimeUnit.SECONDS);
        Thread.sleep(10000);
        scheduledThreadPool.shutdown();
    }
}

使用该线程池的scheduleAtFixedRate方法,延迟1秒钟后每隔3秒执行一次任务,运行结果如下:
在这里插入图片描述

2. 5 Executors 各个方法的弊端:

在这里插入图片描述
阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。

3 生命周期管理,线程池都有哪些状态?

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

任务调度

首先,所有任务的调度都是由execute方法完成的:(workerCount:前线程池的线程数,corePoolSize:基本大小线程数,maximumPoolSize:线程池中允许的最大线程数)

1 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

2 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

3 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

4 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

5 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

在这里插入图片描述

任务缓冲

线程池的本质是对任务和线程的管理:将任务和线程两者解耦,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列:在队列为空时,获取任务的线程会等待队列变为非空;当队列满时,存储任务的线程会等待队列可用。

使用不同的队列可以实现不一样的任务存取策略。阻塞队列的成员有以下:

在这里插入图片描述
任务申请

一种是:任务直接由新创建的线程执行。另一种是:线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。

任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略策略
AbortPolicy丢弃任务,并抛出异常:最大承载=maximumPoolSize + BlockingQueue
CallerRunsPolicy由提交任务的线程处理该任务
DiscardPolicy不处理新任务,直接丢弃掉
DiscardOldestPolicy丢弃队列最前面的任务,然后重新提交被拒绝的任务。

4 创建线程池的七大参数

参数特点
corePoolSize核心线程池大小:线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁。当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。
maximumPoolSize一个任务被提交到线程池以后,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
keepAliveTime一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁;如果allowCoreThreadTimeOut被设置为true时,无论线程数多少,线程处于空闲状态超过一定时间就会被销毁掉
TimeUnit unit超时单位
BlockingQueue workQueue用来保存等待被执行的任务的阻塞队列
ThreadFactory threadFactory为线程池提供创建新线程的线程工厂
RejectedExecutionHandler handler当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,会执行拒绝策略

5 你知道怎么创建线程池吗?

ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。
在这里插入图片描述

public class Test {
    public static void main(String[] args) {
        // 获取cpu 的核数
        int max = Runtime.getRuntime().availableProcessors();
        ExecutorService service =new ThreadPoolExecutor(
                2,
                max,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        try {
            for (int i = 1; i <= 10; i++) {
                service.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "ok");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            service.shutdown();
        }
    }
}

6 ThreadPoolExecutor

Java中的线程池核心实现类是ThreadPoolExecutor。ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?
在这里插入图片描述
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。
任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:
1 直接申请线程执行该任务;
2 缓冲到队列中等待线程执行;
3 拒绝该任务。

线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

7 如何优雅设置线程池的大小

线程池需要设置合适的大小,假如设置的太大,线程上线文切换过于频繁,造成大量资源开销,反而会使性能降低。假如设置的太小,存在很多可用的处理器资源却未在工作,会造成资源的浪费和对吞吐量造成损失。

为了充分利用处理器资源,创建的线程数至少要等于处理器核心数。如果所有的任务都是计算密集型的,那么线程数等于可用的处理器核心数就可以了。不过,如果所有的任务都是IO密集型,那么处理器大部分时间是空闲的,所以要适当的增加线程数。线程等待时间所占比例越高(IO密集型),需要越多线程。线程运算时间所占比例越高(计算密集型),需要越少线程。 于是可以使用下面的公式进行估算:

最佳线程数 =1 + 线程等待时间/线程计算时间)* 目标CPU的使用率 * 处理器核心数

例如:平均每个线程计算运行时间为0.5s,而线程等待时间(非计算时间,比如IO)为1.5s,目标CPU的使用率是90%,CPU核心数为8,那么根据上面这个公式估算得到:(1 + 1.5/0.5) * 90% * 8 = 28.8。

即使有上面的简单估算方法,也许看似合理,但实际上也未必合理,都需要结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯内存操作)和硬件环境(CPU、内存、硬盘读写速度、网络状况等)来不断尝试达到一个符合实际的合理估算值,也可以尝试Dark Magic的估算方法。

8 线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。

9 线程池是如何保持核心线程不被摧毁呢?

1、客户端创建线程池对象后,调用execute() 提交一个Runnable任务;
2、execute()会调用addWorker() 创建一个Worker对象;
3、addWorker()内部会调用Worker.thread.start() 这时候实际调用的就是Worker对象内部的run方法;
4、Worker中的run方法委托给runWorker()执行;
5、runWorker()中有while循环体,不断地调用getTask()获取新任务;
6、在getTask()方法里它就是调用阻塞队列的poll()take()等待获取其中的任务,getTask()通过调用blockQueuetake()获取队列中的任务,如果队列为空,就一直阻塞当前线程,利用了阻塞队列的特性,实现核心线程空闲时间,也保持live;

package com.thread.pool;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static java.lang.Thread.sleep;

public class TestFixedThreadPool {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = getThreadPoolExecutorService_bk();
        fixedThreadPool.shutdown();
    }

    private static ExecutorService getFixedExecutorService() {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        IntStream.rangeClosed(1, 5).forEach(
                item -> fixedThreadPool.execute(() -> {
                        try {
                            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                            System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
                            sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }));
        return fixedThreadPool;
    }

    private static ExecutorService getCachedExecutorService() {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        IntStream.rangeClosed(1, 10).forEach(
                item -> cachedThreadPool.execute(() -> {
                        try {
                            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                            System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
                            sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }}));
        return cachedThreadPool;
    }

    private static ExecutorService getSingleExecutorService() {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        IntStream.rangeClosed(1, 8).forEach(
                item -> singleThreadExecutor.execute(() -> {
                    try {
                        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                        System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
                        sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }}));
        return singleThreadExecutor;
    }

    private static ExecutorService getScheduledExecutorService() {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        System.out.println("提交时间: " + sdf.format(new Date()));
        scheduledThreadPool.schedule(() ->
                System.out.println("运行时间: " + sdf.format(new Date())), 3, TimeUnit.SECONDS);

        scheduledThreadPool.scheduleAtFixedRate(() ->
                System.out.println("运行时间: " + sdf.format(new Date())), 1, 3, TimeUnit.SECONDS);

        return scheduledThreadPool;
    }

    private static ExecutorService getThreadPoolExecutorService() {
        int max = Runtime.getRuntime().availableProcessors();
        System.out.println(max);
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
                max,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        IntStream.rangeClosed(1, 10).forEach(
                item -> threadPoolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok " + sdf.format(new Date()));
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                })
        );
        threadPoolExecutor.shutdown();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(threadPoolExecutor.getPoolSize());
        return threadPoolExecutor;
    }

    /**
     * 由提交任务的线程处理该任务 不会丢弃任务
     * @return
     */
    private static ExecutorService getThreadPoolExecutorService_bk() {
        int max = Runtime.getRuntime().availableProcessors();
        System.out.println(max);
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
                2,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        IntStream.rangeClosed(1, 10).forEach(
                item -> CompletableFuture.supplyAsync(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok " + sdf.format(new Date()));
                    try {
                        sleep(3000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    return null;
                }, threadPoolExecutor)
        );
        threadPoolExecutor.shutdown();
        return threadPoolExecutor;
    }
}

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

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

相关文章

分享3款屏幕录制软件,一定要来看!

在数字化时代&#xff0c;屏幕录制软件成为了一个必不可少的工具。它可以帮助用户捕捉并记录计算机屏幕上的活动&#xff0c;因此选择一款功能强大、易于使用的屏幕录制软件至关重要。本文将介绍3款备受好评的屏幕录制软件&#xff0c;通过本文的阅读&#xff0c;您将了解到它们…

Android图形-刷新与显示

目录 屏幕显示原理&#xff1a; 显示刷新的过程 VSYNC机制具体实现 小结&#xff1a; 屏幕显示原理&#xff1a; 过程描述&#xff1a; 应用向系统服务申请buffer 系统服务返回一个buffer给应用 应用开始绘制&#xff0c;绘制完成就提交buffer&#xff0c;系统服务把buffer数据…

两年了^

我也想不到是到了现在 记得刚来腾讯工作半年时候&#xff0c;我写了一篇文章 在腾讯的这半年 之后&#xff0c;又经过了半年时间&#xff0c;我又写了一篇总结文章 一年了 现在又过了一年多&#xff0c;本想把两年的入职截图留下&#xff0c;之前因为微信存储太大把微信卸载后重…

实现功能:ChatGPT 微信助手可以自动搜索网络信息回答问题

“ ChatGPT微信助手升级内测功能上线&#xff01;原先只能回答通用问题,现在遇到需要查询具体信息的问题也不愁啦。” 01 — 最近&#xff0c;上线了ChatGPT微信助手的体验群&#xff1a;《ChatGPT 微信助手上线&#xff01;问答更便捷&#xff0c;功能持续升级中。》&#xff…

Android高手进阶教程(一)-------Android常用名令集锦(图文并茂)!

大家好&#xff0c;今天我们要讲的是android开发中&#xff0c;比较常用的名令集锦&#xff0c; 在我们开发中难免用到Android命令&#xff0c;有些确实命令确实很有用处。 特别对于一些初学者来说&#xff0c;命令根本没有想过用也不会用&#xff0c;比如他们想安装一个.apk文…

以太网网络安全协议(十三)

一、IPsec协议 IPsec。它是指在IP首部的后面追加“封装安全有效载荷”&#xff08;ESP&#xff09;和“认证首部”&#xff08;AH&#xff09; &#xff0c;从而对此后的数据进行加密&#xff0c;不被盗取者轻易解读。 二、TLS/SLL协议 SSL最早由网景公司提出&#xff0c;标准化…

架构设计第42讲:美团 - 可视化全链路日志追踪

架构设计第42讲&#xff1a;美团 - 可视化全链路日志追踪 目前在分布式场景下&#xff0c;业务追踪的主流实现方式包括两类&#xff0c;一类是基于日志的ELK方案&#xff0c;一类是基于单次请求调用的会话跟踪方案。然而随着业务逻辑的日益复杂&#xff0c;上述方案越来越不适用…

8月10日,每日信息差

1、菜鸟国际快递“承诺达、晚必赔”覆盖22国。具体来讲&#xff0c;菜鸟国际快递对无忧、经济和简易三种标准产品全面升级&#xff0c;其中英国、意大利、加拿大、美国四国时效平均提升30%以上。凡使用这三种快递产品发货至22国的商家&#xff0c;其货物在速卖通电商平台享有“…

网络安全 Day29-运维安全项目-iptables防火墙

iptables防火墙 1. 防火墙概述2. 防火墙2.1 防火墙种类及使用说明2.2 必须熟悉的名词2.3 iptables 执行过程※※※※※2.4 表与链※※※※※2.4.1 简介2.4.2 每个表说明2.4.2.1 filter表 :star::star::star::star::star:2.4.2.2 nat表 2.5 环境准备及命令2.6 案例01&#xff1a…

月度资金预算情况表取的数和结算页面不一样,是为什么?

月度资金预算情况表取的数和结算页面不一样&#xff0c;如下图&#xff0c;是为什么&#xff1f; 取数公式&#xff1a; CMPRFS(‘[收支项目01030101] ‘,K(‘单位’),’’,‘’,‘0’,ZDATEQC(‘-’),ZDATE(‘-’),‘1’,‘’,‘’)CMPRFS(‘[收支项目01030102] ‘,K(‘单位…

最新版本2023UI千月影视APP源码 开源完美版前后端完美匹配 后端基于ThinkPHP框架

最新版本的2023UI千月影视APP源码是一款开源的完美版应用程序&#xff0c;具备前后端完美匹配的特点。该应用的后端开发基于ThinkPHP框架&#xff0c;这是一个广泛使用的PHP开发框架&#xff0c;具有稳定性和安全性方面的优势。 2023UI千月影视APP是一款提供电影、电视剧、综艺…

文旅虚拟IP内卷国风,底层流量密码是什么?

数字化浪潮下&#xff0c;文旅虚拟IP已逐渐成为数字经济时代全新的增长风口以及传统文化传播载体。 虚拟IP具有极高的辨识度和可塑性&#xff0c;既可以作为虚拟主播在线上直播聊天互动&#xff0c;又可以化身虚拟解说员带你学习传统文化&#xff0c;还可以化身虚拟向导带你玩…

我与金融 —— 境外支付系统之安全测试实践(一)

&#x1f60f;作者简介&#xff1a;博主是一位测试管理者&#xff0c;同时也是一名对外企业兼职讲师。 &#x1f4e1;主页地址&#xff1a;【Austin_zhai】 &#x1f646;目的与景愿&#xff1a;旨在于能帮助更多的测试行业人员提升软硬技能&#xff0c;分享行业相关最新信息。…

【雕爷学编程】Arduino动手做(12)---霍尔模块之霍尔磁感应声光报警器(磁控开关,接220V)

37款传感器与模块的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&#x…

前端探索之旅

目录 简介:内容大纲:第一章 前端开发简介1.1 前端开发的定义和作用1.2 前端开发的职责1.3 前端开发的技能要求1.4 前端开发的发展前景总结&#xff1a; 第二章 HTML基础2.1 HTML基本结构2.2 常见HTML标签和元素 第三章 CSS基础3.1 CSS基本语法3.2 常见CSS选择器3.3 常见CSS属性…

基于Java+SpringBoot+Vue的网上图书商城设计与实现(源码+LW+部署文档等)

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

销售线索管理软件有哪些?4类线索管理平台推荐

一、销售线索管理&#xff0c;管的是什么&#xff1f; 销售线索&#xff0c;在营销术语里又被成为leads&#xff0c;一般是潜在客户的关键信息&#xff0c;如客户的名称、联系方式、行业、痛点或需求等&#xff0c;处在整个销售流程的最前端&#xff0c;是销售工作开展的基础。…

linux自启动程序

嵌入式linux下有软件需要自启动&#xff0c;只需要在/etc/init.d/rcS末尾添加所要启动的程序即可&#xff0c;开机就会自动运行 vi /etc/init.d/rcS在文件末尾添加 例&#xff1a;

Spring Data JPA 详解

目录 一、概述1.1 JPA简介1.2 Spring Data JPA简介 二、配置及应用2.1 环境配置2.2 依赖添加2.3 实体类创建2.4 Repository接口创建2.5 示例程序运行 三、实体映射3.1 注解3.2 关系映射 四、Repository接口4.1 基本增删改查4.2 自定义查询方法4.3 使用 Sort 和 Pageable 进行排…

八月组队学习来了!

Datawhale学习 联合主办&#xff1a;Datawhale&#xff0c;人民邮电出版社 本期学习由Datawhale和人民邮电出版社异步社区联合发起&#xff0c;学习大纲如下&#xff08;文末整理了这次学习的所有资料&#xff09;&#xff1a; 参与学习 ▶ 活动时间&#xff1a;学习活动 8月1…